Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add audit log output #111

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
06f8ed1
Add audit_log output
Laugslander Dec 13, 2023
0fb5e6d
Update dist
Laugslander Dec 13, 2023
24293f5
Property shorthand
Laugslander Dec 13, 2023
23865ee
Improve typing
Laugslander Dec 13, 2023
32ba867
Run prettier
Laugslander Dec 13, 2023
59cd832
Update dist
Laugslander Dec 13, 2023
59c274a
Export variable
Laugslander Dec 13, 2023
672202c
Update dist
Laugslander Dec 13, 2023
439fe16
Set output
Laugslander Dec 13, 2023
eece747
actions/checkout@v4
Laugslander Dec 13, 2023
50a64c7
Improve test
Laugslander Dec 13, 2023
e15e35a
@actions/core:^1.1.0
Laugslander Dec 13, 2023
dd0d975
Improve test
Laugslander Dec 13, 2023
c7e1822
Update dist
Laugslander Dec 13, 2023
0df87bb
Replace tweetsodium with libsodium and hash secrets
Laugslander Dec 14, 2023
169a0d0
Revert tweetsodium replacement
Laugslander Dec 14, 2023
4599c97
Update packages
Laugslander Dec 14, 2023
d8f8180
Update dist
Laugslander Dec 14, 2023
3e14f53
Mock @actions/core to suppress core.setFailed error logging
Laugslander Dec 14, 2023
59f42f1
Run prettier
Laugslander Dec 14, 2023
dffac76
Test audit log
Laugslander Dec 19, 2023
5588475
Update docs
Laugslander Dec 19, 2023
8b090f6
Switch to SHA 512
Laugslander Dec 19, 2023
f8e7284
Add salt input for hash function
Laugslander Dec 19, 2023
fdc5a09
"@typescript-eslint/no-unused-vars": "off"
Laugslander Dec 19, 2023
d07aadc
Update AuditLog property ordering
Laugslander Dec 19, 2023
1db1800
Order variables
Laugslander Dec 19, 2023
000a539
Address review comments
Laugslander Dec 20, 2023
36f6627
Remove unused import
Laugslander Jan 3, 2024
bad1d82
Use GITHUB_REPOSITORY_ID as default salt
Laugslander Jan 8, 2024
c39ca3b
Run prettier
Laugslander Jan 8, 2024
5f42733
Update dist
Laugslander Jan 8, 2024
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
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- run: |
npm i
npm run all
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
ref: 'master'
- name: Keep dist up-to-date
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ If this value is set to the name of a valid environment in the target repositori

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

### `audit_log_hashing_salt`

Salt for hashing the secrets in the audit log.

## Outputs

### `audit_log`

Audit log (JSON structure) describing which secrets have been updated in which repository.

## 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 AUDIT_LOG_HASHING_SALT = "salt";

// 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_AUDIT_LOG_HASHING_SALT: 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_AUDIT_LOG_HASHING_SALT: String(AUDIT_LOG_HASHING_SALT),
};

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

Expand Down
123 changes: 108 additions & 15 deletions __tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import * as config from "../src/config";
import * as utils from "../src/utils";

import {
DefaultOctokit,
Expand Down Expand Up @@ -166,6 +167,8 @@ describe("setSecretForRepo", () => {
return body;
})
.reply(200);

(utils.hash as jest.Mock) = jest.fn().mockImplementation(() => "hash");
});

test("setSecretForRepo with Actions target should retrieve public key for Actions", async () => {
Expand All @@ -176,7 +179,8 @@ describe("setSecretForRepo", () => {
repo,
"",
true,
"actions"
"actions",
"salt"
);
expect(actionsPublicKeyMock.isDone()).toBeTruthy();
});
Expand All @@ -189,7 +193,8 @@ describe("setSecretForRepo", () => {
repo,
"",
true,
"dependabot"
"dependabot",
"salt"
);
expect(dependabotPublicKeyMock.isDone()).toBeTruthy();
});
Expand All @@ -202,7 +207,8 @@ describe("setSecretForRepo", () => {
repo,
"",
true,
"actions"
"actions",
"salt"
);
expect(actionsPublicKeyMock.isDone()).toBeTruthy();
expect(setActionsSecretMock.isDone()).toBeFalsy();
Expand All @@ -216,7 +222,8 @@ describe("setSecretForRepo", () => {
repo,
"",
false,
"actions"
"actions",
"salt"
);
expect(setActionsSecretMock.isDone()).toBeTruthy();
});
Expand All @@ -229,10 +236,36 @@ describe("setSecretForRepo", () => {
repo,
"",
false,
"dependabot"
"dependabot",
"salt"
);
expect(setDependabotSecretMock.isDone()).toBeTruthy();
});

test("setSecretForRepo should return AuditLog", async () => {
const secret_name = "FOO";
const environment = "";
const dry_run = false;
const target = "actions";

const auditLog = await setSecretForRepo(
octokit,
secret_name,
secrets.FOO,
repo,
environment,
dry_run,
target,
"salt"
);
expect(auditLog.action).toEqual("set");
expect(auditLog.repo).toEqual(repo.full_name);
expect(auditLog.target).toEqual(target);
expect(auditLog.secret_name).toEqual(secret_name);
expect(auditLog.secret_hash).not.toBeNull();
expect(auditLog.environment).toEqual(environment);
expect(auditLog.dry_run).toEqual(dry_run);
});
});

describe("setSecretForRepo with environment", () => {
Expand Down Expand Up @@ -271,6 +304,8 @@ describe("setSecretForRepo with environment", () => {
}
)
.reply(200);

(utils.hash as jest.Mock) = jest.fn().mockImplementation(() => "hash");
});

test("setSecretForRepo should retrieve public key", async () => {
Expand All @@ -281,7 +316,8 @@ describe("setSecretForRepo with environment", () => {
repo,
repoEnvironment,
true,
"actions"
"actions",
"salt"
);
expect(environmentPublicKeyMock.isDone()).toBeTruthy();
});
Expand All @@ -294,7 +330,8 @@ describe("setSecretForRepo with environment", () => {
repo,
repoEnvironment,
true,
"actions"
"actions",
"salt"
);
expect(environmentPublicKeyMock.isDone()).toBeTruthy();
expect(setEnvironmentSecretMock.isDone()).toBeFalsy();
Expand All @@ -308,7 +345,8 @@ describe("setSecretForRepo with environment", () => {
repo,
repoEnvironment,
true,
"dependabot"
"dependabot",
"salt"
);
expect(environmentPublicKeyMock.isDone()).toBeTruthy();
expect(setEnvironmentSecretMock.isDone()).toBeFalsy();
Expand All @@ -322,10 +360,35 @@ describe("setSecretForRepo with environment", () => {
repo,
repoEnvironment,
false,
"actions"
"actions",
"salt"
);
expect(nock.isDone()).toBeTruthy();
});

test("setSecretForRepo should return AuditLog", async () => {
const secret_name = "FOO";
const dry_run = false;
const target = "actions";

const auditLog = await setSecretForRepo(
octokit,
secret_name,
secrets.FOO,
repo,
repoEnvironment,
dry_run,
target,
"salt"
);
expect(auditLog.action).toEqual("set");
expect(auditLog.repo).toEqual(repo.full_name);
expect(auditLog.target).toEqual(target);
expect(auditLog.secret_name).toEqual(secret_name);
expect(auditLog.secret_hash).not.toBeNull();
expect(auditLog.environment).toEqual(repoEnvironment);
expect(auditLog.dry_run).toEqual(dry_run);
});
});

describe("deleteSecretForRepo", () => {
Expand Down Expand Up @@ -357,7 +420,8 @@ describe("deleteSecretForRepo", () => {
repo,
"",
true,
"actions"
"actions",
"salt"
);
expect(deleteActionsSecretMock.isDone()).toBeFalsy();
});
Expand All @@ -370,7 +434,8 @@ describe("deleteSecretForRepo", () => {
repo,
"",
false,
"actions"
"actions",
"salt"
);
expect(deleteActionsSecretMock.isDone()).toBeTruthy();
});
Expand All @@ -383,7 +448,8 @@ describe("deleteSecretForRepo", () => {
repo,
"",
false,
"dependabot"
"dependabot",
"salt"
);
expect(deleteDependabotSecretMock.isDone()).toBeTruthy();
});
Expand Down Expand Up @@ -416,7 +482,8 @@ describe("deleteSecretForRepo with environment", () => {
repo,
repoEnvironment,
true,
"actions"
"actions",
"salt"
);
expect(deleteSecretMock.isDone()).toBeFalsy();
});
Expand All @@ -429,7 +496,8 @@ describe("deleteSecretForRepo with environment", () => {
repo,
repoEnvironment,
true,
"dependabot"
"dependabot",
"salt"
);
expect(deleteSecretMock.isDone()).toBeFalsy();
});
Expand All @@ -442,8 +510,33 @@ describe("deleteSecretForRepo with environment", () => {
repo,
repoEnvironment,
false,
"actions"
"actions",
"salt"
);
expect(nock.isDone()).toBeTruthy();
});

test("deleteSecretForRepo should return AuditLog", async () => {
const secret_name = "FOO";
const dry_run = false;
const target = "actions";

const auditLog = await deleteSecretForRepo(
octokit,
secret_name,
secrets.FOO,
repo,
repoEnvironment,
dry_run,
target,
"salt"
);
expect(auditLog.action).toEqual("delete");
expect(auditLog.repo).toEqual(repo.full_name);
expect(auditLog.target).toEqual(target);
expect(auditLog.secret_name).toEqual(secret_name);
expect(auditLog.secret_hash).not.toBeNull();
expect(auditLog.environment).toEqual(repoEnvironment);
expect(auditLog.dry_run).toEqual(dry_run);
});
});
1 change: 1 addition & 0 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as secrets from "../src/secrets";
import fixture from "@octokit/fixtures/scenarios/api.github.com/get-repository/normalized-fixture.json";
import nock from "nock";
import { run } from "../src/main";
import { setFailed } from "@actions/core";
Laugslander marked this conversation as resolved.
Show resolved Hide resolved

nock.disableNetConnect();

Expand Down
28 changes: 27 additions & 1 deletion __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,36 @@
* limitations under the License.
*/

import { encrypt } from "../src/utils";
import { encrypt, hash } from "../src/utils";

const key = "HRkzRZD1+duhfvNvY8eiCPb+ihIjbvkvRyiehJCs8Vc=";

test("encrypt should return a value", () => {
expect(encrypt("baz", key)).toBeTruthy();
});

test("hashing algorithm golden standard", async () => {
const value = "baz";
const salt = "salt";
const hashed_value = hash(value, salt);

// After making changes to the hashing algorithm, this output should stay intact.
expect(hashed_value).toEqual("b6c1ba0fdd");
});

test("hashing the same value should return the same result", async () => {
const value = "baz";
const salt = "salt";
const hashed_value_1 = hash(value, salt);
const hashed_value_2 = hash(value, salt);

expect(hashed_value_1).toEqual(hashed_value_2);
});

test("hashing a different value should return a different result", async () => {
const salt = "salt";
const hashed_value_1 = hash("bar", salt);
const hashed_value_2 = hash("baz", salt);

expect(hashed_value_1).not.toEqual(hashed_value_2);
});
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ inputs:
Target where secrets should be stored: `actions` (default) or `dependabot`.
default: "actions"
required: false
audit_log_hashing_salt:
default: ""
description: |
Salt for hashing the secrets in the audit log.
outputs:
audit_log:
description: |
Audit log (JSON structure) describing which secrets have been updated in which repository.
runs:
using: 'node16'
main: 'dist/index.js'
Loading
Loading