mirror of
https://github.com/step-security/harden-runner.git
synced 2026-06-05 19:53:33 +00:00
Fix policy store to use correct API endpoint and auth scheme
Use /actions/policies/workflow-policy with query params (workflow, run_id, correlationId) and vm-api-key auth header to match the existing backend API. Update action.yml descriptions to clarify policy store is the preferred method.
This commit is contained in:
parent
bf693cb624
commit
85b3620336
6 changed files with 56 additions and 27 deletions
|
|
@ -29,15 +29,15 @@ inputs:
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
policy:
|
policy:
|
||||||
description: "Policy name to be used from the policy store"
|
description: "Policy name to be used from the policy store. Requires id-token: write permission."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
api-key:
|
api-key:
|
||||||
description: "StepSecurity API key for authenticating with the policy store"
|
description: "StepSecurity API key for authenticating with the policy store. Required when use-policy-store is set to true."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
use-policy-store:
|
use-policy-store:
|
||||||
description: "Set to true to fetch policy from the policy store using the API key"
|
description: "Set to true to fetch policy from the policy store using the API key. This is the preferred method over the policy input which requires id-token: write permission. Policies can be defined and attached at workflow, repo, org, or cluster (for ARC) level in the policy store. The most granular policy will apply."
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
|
|
|
||||||
9
dist/pre/index.js
vendored
9
dist/pre/index.js
vendored
|
|
@ -85262,15 +85262,15 @@ function fetchPolicy(owner, policyName, idToken) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function fetchPolicyFromStore(owner, repo, apiKey) {
|
function fetchPolicyFromStore(owner, repo, apiKey, workflow, runId, correlationId) {
|
||||||
return policy_utils_awaiter(this, void 0, void 0, function* () {
|
return policy_utils_awaiter(this, void 0, void 0, function* () {
|
||||||
if (apiKey === "") {
|
if (apiKey === "") {
|
||||||
throw new Error("[PolicyStoreFetch]: api-key is empty");
|
throw new Error("[PolicyStoreFetch]: api-key is empty");
|
||||||
}
|
}
|
||||||
let policyEndpoint = `${configs_STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/policy-store/policy`;
|
let policyEndpoint = `${configs_STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/policies/workflow-policy?workflow=${encodeURIComponent(workflow)}&run_id=${encodeURIComponent(runId)}&correlationId=${encodeURIComponent(correlationId)}`;
|
||||||
let httpClient = new lib.HttpClient();
|
let httpClient = new lib.HttpClient();
|
||||||
let headers = {};
|
let headers = {};
|
||||||
headers["Authorization"] = `api-key ${apiKey}`;
|
headers["Authorization"] = `vm-api-key ${apiKey}`;
|
||||||
headers["Source"] = "github-actions";
|
headers["Source"] = "github-actions";
|
||||||
let response = undefined;
|
let response = undefined;
|
||||||
let err = undefined;
|
let err = undefined;
|
||||||
|
|
@ -85733,7 +85733,8 @@ var setup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _ar
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
|
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
|
||||||
let result = yield fetchPolicyFromStore(github.context.repo.owner, repoName, confg.api_key);
|
const workflow = process.env["GITHUB_WORKFLOW"] || "";
|
||||||
|
let result = yield fetchPolicyFromStore(github.context.repo.owner, repoName, confg.api_key, workflow, confg.run_id, confg.correlation_id);
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
confg = mergeConfigs(confg, result);
|
confg = mergeConfigs(confg, result);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
dist/pre/index.js.map
vendored
2
dist/pre/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -146,9 +146,15 @@ test("fetchPolicy preserves statusCode from error", async () => {
|
||||||
|
|
||||||
import { fetchPolicyFromStore } from "./policy-utils";
|
import { fetchPolicyFromStore } from "./policy-utils";
|
||||||
|
|
||||||
|
const policyStoreQueryString = (workflow: string, runId: string, correlationId: string) =>
|
||||||
|
`workflow=${encodeURIComponent(workflow)}&run_id=${encodeURIComponent(runId)}&correlationId=${encodeURIComponent(correlationId)}`;
|
||||||
|
|
||||||
test("success: fetches policy from store", async () => {
|
test("success: fetches policy from store", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
const response = {
|
const response = {
|
||||||
allowed_endpoints: ["registry.npmjs.org:443", "github.com:443"],
|
allowed_endpoints: ["registry.npmjs.org:443", "github.com:443"],
|
||||||
egress_policy: "block",
|
egress_policy: "block",
|
||||||
|
|
@ -157,78 +163,90 @@ test("success: fetches policy from store", async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.reply(200, response);
|
.reply(200, response);
|
||||||
|
|
||||||
const result = await fetchPolicyFromStore(owner, repo, "my-api-key");
|
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
|
||||||
expect(result).toStrictEqual(response);
|
expect(result).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetchPolicyFromStore throws when apiKey is empty", async () => {
|
test("fetchPolicyFromStore throws when apiKey is empty", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
fetchPolicyFromStore("owner", "repo", "")
|
fetchPolicyFromStore("owner", "repo", "", "ci.yml", "123", "abc")
|
||||||
).rejects.toThrow("[PolicyStoreFetch]: api-key is empty");
|
).rejects.toThrow("[PolicyStoreFetch]: api-key is empty");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetchPolicyFromStore returns null when policy not found (404)", async () => {
|
test("fetchPolicyFromStore returns null when policy not found (404)", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.reply(404, { message: "not found" });
|
.reply(404, { message: "not found" });
|
||||||
|
|
||||||
const result = await fetchPolicyFromStore(owner, repo, "my-api-key");
|
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetchPolicyFromStore retries on failure and succeeds", async () => {
|
test("fetchPolicyFromStore retries on failure and succeeds", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
const response = {
|
const response = {
|
||||||
allowed_endpoints: ["example.com:443"],
|
allowed_endpoints: ["example.com:443"],
|
||||||
egress_policy: "audit",
|
egress_policy: "audit",
|
||||||
};
|
};
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.replyWithError("timeout");
|
.replyWithError("timeout");
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.reply(200, response);
|
.reply(200, response);
|
||||||
|
|
||||||
const result = await fetchPolicyFromStore(owner, repo, "my-api-key");
|
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
|
||||||
expect(result).toStrictEqual(response);
|
expect(result).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetchPolicyFromStore throws after all retries exhausted", async () => {
|
test("fetchPolicyFromStore throws after all retries exhausted", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.times(3)
|
.times(3)
|
||||||
.replyWithError("connection refused");
|
.replyWithError("connection refused");
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
fetchPolicyFromStore(owner, repo, "my-api-key")
|
fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId)
|
||||||
).rejects.toThrow("[Policy Store Fetch]");
|
).rejects.toThrow("[Policy Store Fetch]");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetchPolicyFromStore preserves statusCode from error", async () => {
|
test("fetchPolicyFromStore preserves statusCode from error", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
|
|
||||||
const errorWithStatus = new Error("Unauthorized");
|
const errorWithStatus = new Error("Unauthorized");
|
||||||
(errorWithStatus as any).statusCode = 401;
|
(errorWithStatus as any).statusCode = 401;
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`)
|
nock(`${STEPSECURITY_API_URL}`)
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.times(3)
|
.times(3)
|
||||||
.replyWithError(errorWithStatus);
|
.replyWithError(errorWithStatus);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetchPolicyFromStore(owner, repo, "my-api-key");
|
await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
|
||||||
fail("should have thrown");
|
fail("should have thrown");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err.message).toContain("[Policy Store Fetch]");
|
expect(err.message).toContain("[Policy Store Fetch]");
|
||||||
|
|
@ -239,17 +257,20 @@ test("fetchPolicyFromStore sends correct authorization header", async () => {
|
||||||
const owner = "test-owner";
|
const owner = "test-owner";
|
||||||
const repo = "test-repo";
|
const repo = "test-repo";
|
||||||
const apiKey = "secret-key-123";
|
const apiKey = "secret-key-123";
|
||||||
|
const workflow = "ci.yml";
|
||||||
|
const runId = "12345";
|
||||||
|
const correlationId = "abc-def";
|
||||||
|
|
||||||
nock(`${STEPSECURITY_API_URL}`, {
|
nock(`${STEPSECURITY_API_URL}`, {
|
||||||
reqheaders: {
|
reqheaders: {
|
||||||
Authorization: `api-key ${apiKey}`,
|
Authorization: `vm-api-key ${apiKey}`,
|
||||||
Source: "github-actions",
|
Source: "github-actions",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.get(`/github/${owner}/${repo}/actions/policy-store/policy`)
|
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
|
||||||
.reply(200, { allowed_endpoints: [], egress_policy: "audit" });
|
.reply(200, { allowed_endpoints: [], egress_policy: "audit" });
|
||||||
|
|
||||||
const result = await fetchPolicyFromStore(owner, repo, apiKey);
|
const result = await fetchPolicyFromStore(owner, repo, apiKey, workflow, runId, correlationId);
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
allowed_endpoints: [],
|
allowed_endpoints: [],
|
||||||
egress_policy: "audit",
|
egress_policy: "audit",
|
||||||
|
|
|
||||||
|
|
@ -53,18 +53,21 @@ export async function fetchPolicy(
|
||||||
export async function fetchPolicyFromStore(
|
export async function fetchPolicyFromStore(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
apiKey: string
|
apiKey: string,
|
||||||
|
workflow: string,
|
||||||
|
runId: string,
|
||||||
|
correlationId: string
|
||||||
): Promise<PolicyResponse | null> {
|
): Promise<PolicyResponse | null> {
|
||||||
if (apiKey === "") {
|
if (apiKey === "") {
|
||||||
throw new Error("[PolicyStoreFetch]: api-key is empty");
|
throw new Error("[PolicyStoreFetch]: api-key is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
let policyEndpoint = `${STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/policy-store/policy`;
|
let policyEndpoint = `${STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/policies/workflow-policy?workflow=${encodeURIComponent(workflow)}&run_id=${encodeURIComponent(runId)}&correlationId=${encodeURIComponent(correlationId)}`;
|
||||||
|
|
||||||
let httpClient = new HttpClient();
|
let httpClient = new HttpClient();
|
||||||
|
|
||||||
let headers = {};
|
let headers = {};
|
||||||
headers["Authorization"] = `api-key ${apiKey}`;
|
headers["Authorization"] = `vm-api-key ${apiKey}`;
|
||||||
headers["Source"] = "github-actions";
|
headers["Source"] = "github-actions";
|
||||||
|
|
||||||
let response = undefined;
|
let response = undefined;
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,14 @@ interface MonitorResponse {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
|
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
|
||||||
|
const workflow = process.env["GITHUB_WORKFLOW"] || "";
|
||||||
let result: PolicyResponse | null = await fetchPolicyFromStore(
|
let result: PolicyResponse | null = await fetchPolicyFromStore(
|
||||||
context.repo.owner,
|
context.repo.owner,
|
||||||
repoName,
|
repoName,
|
||||||
confg.api_key
|
confg.api_key,
|
||||||
|
workflow,
|
||||||
|
confg.run_id,
|
||||||
|
confg.correlation_id
|
||||||
);
|
);
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
confg = mergeConfigs(confg, result);
|
confg = mergeConfigs(confg, result);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue