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:
Varun Sharma 2026-04-03 20:45:39 -07:00
commit 85b3620336
6 changed files with 56 additions and 27 deletions

View file

@ -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
View file

@ -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);
} }

File diff suppressed because one or more lines are too long

View file

@ -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",

View file

@ -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;

View file

@ -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);