1
0
Fork 0
mirror of synced 2026-06-05 17:55:14 +00:00

Compare commits

...

6 commits

Author SHA1 Message Date
Varun Sharma
289f73efea Exclude api_key from agent config to avoid self-hosted auth path
The Go agent uses api_key for self-hosted authentication. Passing
the policy store api_key into agent.json caused the agent to use
self-hosted upload paths, resulting in missing process and file events.
2026-04-08 18:04:39 -07:00
Varun Sharma
8e8c444a73 Fix workflow name extraction for policy store API
Use GITHUB_WORKFLOW_REF to extract the workflow filename instead of
GITHUB_WORKFLOW which gives the workflow display name. The policy
store API expects the filename (e.g. ci.yml) to match workflow-level
policies.
2026-04-08 16:38:21 -07:00
Varun Sharma
74e0b3a07e Mask api-key in logs and print policy name when found 2026-04-03 22:20:59 -07:00
Varun Sharma
92af6d7ec3 Handle empty policy response as no policy found
API returns 200 with empty policy (no egress_policy, no endpoints)
when no policy exists for a repo. Treat this as no policy found and
default to audit mode. Update action.yml descriptions for policy store.
2026-04-03 20:48:24 -07:00
Varun Sharma
85b3620336 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.
2026-04-03 20:45:39 -07:00
Varun Sharma
bf693cb624 Add policy store support with api-key authentication
Adds use-policy-store and api-key inputs to allow fetching policies
from the StepSecurity policy store. Defaults to audit mode when no
policy is found. Includes unit tests with 100% coverage.
2026-04-03 19:59:54 -07:00
7 changed files with 521 additions and 7 deletions

View file

@ -29,9 +29,17 @@ 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:
description: "StepSecurity API key for authenticating with the policy store. Required when use-policy-store is set to true."
required: false
default: ""
use-policy-store:
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
default: "false"
branding: branding:
icon: "check-square" icon: "check-square"

96
dist/pre/index.js vendored
View file

@ -85262,6 +85262,48 @@ function fetchPolicy(owner, policyName, idToken) {
} }
}); });
} }
function fetchPolicyFromStore(owner, repo, apiKey, workflow, runId, correlationId) {
return policy_utils_awaiter(this, void 0, void 0, function* () {
if (apiKey === "") {
throw new Error("[PolicyStoreFetch]: api-key is empty");
}
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 headers = {};
headers["Authorization"] = `vm-api-key ${apiKey}`;
headers["Source"] = "github-actions";
let response = undefined;
let err = undefined;
let retry = 0;
while (retry < 3) {
try {
console.log(`Attempt: ${retry + 1}`);
response = yield httpClient.getJson(policyEndpoint, headers);
break;
}
catch (e) {
err = e;
}
retry += 1;
yield sleep(1000);
}
if (response === undefined && err !== undefined) {
const error = new Error(`[Policy Store Fetch] ${err}`);
if (err.statusCode !== undefined) {
error.statusCode = err.statusCode;
}
throw error;
}
if (response.statusCode === 404) {
return null;
}
const result = response.result;
if (!result || (!result.egress_policy && (!result.allowed_endpoints || result.allowed_endpoints.length === 0))) {
return null;
}
return result;
});
}
function mergeConfigs(localConfig, remoteConfig) { function mergeConfigs(localConfig, remoteConfig) {
if (localConfig.allowed_endpoints === "") { if (localConfig.allowed_endpoints === "") {
localConfig.allowed_endpoints = remoteConfig.allowed_endpoints.join(" "); localConfig.allowed_endpoints = remoteConfig.allowed_endpoints.join(" ");
@ -85624,6 +85666,17 @@ var setup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _ar
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __rest = (undefined && undefined.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
@ -85683,9 +85736,47 @@ var setup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _ar
is_github_hosted: isGithubHosted(), is_github_hosted: isGithubHosted(),
is_debug: lib_core.isDebug(), is_debug: lib_core.isDebug(),
one_time_key: "", one_time_key: "",
api_key: lib_core.getInput("api-key"),
use_policy_store: lib_core.getBooleanInput("use-policy-store"),
}; };
if (confg.api_key !== "") {
lib_core.setSecret(confg.api_key);
}
let policyName = lib_core.getInput("policy"); let policyName = lib_core.getInput("policy");
if (policyName !== "") { if (confg.use_policy_store) {
console.log(`Fetching policy from policy store`);
if (confg.api_key === "") {
lib_core.setFailed("api-key is required when use-policy-store is set to true");
}
else {
try {
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
const workflowRef = process.env["GITHUB_WORKFLOW_REF"] || "";
const workflow = workflowRef.replace(/.*\.github\/workflows\//, "").replace(/@.*/, "");
let result = yield fetchPolicyFromStore(github.context.repo.owner, repoName, confg.api_key, workflow, confg.run_id, confg.correlation_id);
if (result !== null) {
lib_core.info(`Policy found: ${result.policy_name || "unnamed"}`);
confg = mergeConfigs(confg, result);
}
else {
lib_core.info("No policy found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
}
}
catch (err) {
lib_core.info(`[!] ${err}`);
if (err.statusCode >= 400 && err.statusCode < 500) {
lib_core.info("Policy not found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
}
else {
lib_core.error(`Unexpected error fetching from policy store: ${err}. Falling back to audit mode.`);
confg.egress_policy = "audit";
}
}
}
}
else if (policyName !== "") {
console.log(`Fetching policy from API with name: ${policyName}`); console.log(`Fetching policy from API with name: ${policyName}`);
try { try {
let idToken = yield lib_core.getIDToken(); let idToken = yield lib_core.getIDToken();
@ -85858,7 +85949,8 @@ var setup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _ar
console.log(HARDEN_RUNNER_UNAVAILABLE_MESSAGE); console.log(HARDEN_RUNNER_UNAVAILABLE_MESSAGE);
return; return;
} }
const configStr = JSON.stringify(confg); const { api_key, use_policy_store } = confg, agentConfig = __rest(confg, ["api_key", "use_policy_store"]);
const configStr = JSON.stringify(agentConfig);
// platform specific // platform specific
let statusFile = ""; let statusFile = "";
let logFile = ""; let logFile = "";

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,8 @@ export interface Configuration {
private: string; private: string;
is_debug: boolean; is_debug: boolean;
one_time_key: string; one_time_key: string;
api_key: string;
use_policy_store: boolean;
} }
export interface PolicyResponse { export interface PolicyResponse {
@ -26,4 +28,5 @@ export interface PolicyResponse {
disable_file_monitoring?: boolean; disable_file_monitoring?: boolean;
disable_telemetry?: boolean; disable_telemetry?: boolean;
egress_policy?: string; egress_policy?: string;
policy_name?: string;
} }

View file

@ -43,6 +43,8 @@ test("merge configs", async () => {
is_github_hosted: true, is_github_hosted: true,
is_debug: false, is_debug: false,
one_time_key: "", one_time_key: "",
api_key: "",
use_policy_store: false,
}; };
let policyResponse: PolicyResponse = { let policyResponse: PolicyResponse = {
owner: "h0x0er", owner: "h0x0er",
@ -71,8 +73,316 @@ test("merge configs", async () => {
is_github_hosted: true, is_github_hosted: true,
is_debug: false, is_debug: false,
one_time_key: "", one_time_key: "",
api_key: "",
use_policy_store: false,
}; };
localConfig = mergeConfigs(localConfig, policyResponse); localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig).toStrictEqual(expectedConfiguration); expect(localConfig).toStrictEqual(expectedConfiguration);
}); });
// ==================== additional fetchPolicy tests ====================
test("fetchPolicy throws when idToken is empty", async () => {
await expect(fetchPolicy("owner", "policy1", "")).rejects.toThrow(
"[PolicyFetch]: id-token in empty"
);
});
test("fetchPolicy retries on failure and succeeds", async () => {
const owner = "test-owner";
const policyName = "test-policy";
const response = {
allowed_endpoints: ["example.com:443"],
egress_policy: "block",
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.replyWithError("connection timeout");
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.reply(200, response);
const policy = await fetchPolicy(owner, policyName, "token123");
expect(policy).toStrictEqual(response);
});
test("fetchPolicy throws after all retries exhausted", async () => {
const owner = "test-owner";
const policyName = "test-policy";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.times(3)
.replyWithError("connection timeout");
await expect(
fetchPolicy(owner, policyName, "token123")
).rejects.toThrow("[Policy Fetch]");
});
test("fetchPolicy preserves statusCode from error", async () => {
const owner = "test-owner";
const policyName = "test-policy";
const errorWithStatus = new Error("Not Found");
(errorWithStatus as any).statusCode = 404;
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.times(3)
.replyWithError(errorWithStatus);
try {
await fetchPolicy(owner, policyName, "token123");
fail("should have thrown");
} catch (err) {
expect(err.message).toContain("[Policy Fetch]");
}
});
// ==================== fetchPolicyFromStore ====================
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 () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const response = {
allowed_endpoints: ["registry.npmjs.org:443", "github.com:443"],
egress_policy: "block",
disable_sudo: true,
disable_file_monitoring: false,
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, response);
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toStrictEqual(response);
});
test("fetchPolicyFromStore throws when apiKey is empty", async () => {
await expect(
fetchPolicyFromStore("owner", "repo", "", "ci.yml", "123", "abc")
).rejects.toThrow("[PolicyStoreFetch]: api-key is empty");
});
test("fetchPolicyFromStore returns null when policy not found (404)", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(404, { message: "not found" });
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toBeNull();
});
test("fetchPolicyFromStore returns null when API returns empty policy", async () => {
const owner = "test-owner";
const repo = "nonexistent-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, { allowed_endpoints: [], egress_policy: "", policy_name: "" });
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toBeNull();
});
test("fetchPolicyFromStore retries on failure and succeeds", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const response = {
allowed_endpoints: ["example.com:443"],
egress_policy: "audit",
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.replyWithError("timeout");
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, response);
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toStrictEqual(response);
});
test("fetchPolicyFromStore throws after all retries exhausted", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.times(3)
.replyWithError("connection refused");
await expect(
fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId)
).rejects.toThrow("[Policy Store Fetch]");
});
test("fetchPolicyFromStore preserves statusCode from error", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const errorWithStatus = new Error("Unauthorized");
(errorWithStatus as any).statusCode = 401;
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.times(3)
.replyWithError(errorWithStatus);
try {
await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
fail("should have thrown");
} catch (err) {
expect(err.message).toContain("[Policy Store Fetch]");
}
});
test("fetchPolicyFromStore sends correct authorization header", async () => {
const owner = "test-owner";
const repo = "test-repo";
const apiKey = "secret-key-123";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`, {
reqheaders: {
Authorization: `vm-api-key ${apiKey}`,
Source: "github-actions",
},
})
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, { allowed_endpoints: [], egress_policy: "audit" });
const result = await fetchPolicyFromStore(owner, repo, apiKey, workflow, runId, correlationId);
expect(result).toStrictEqual({
allowed_endpoints: [],
egress_policy: "audit",
});
});
// ==================== additional mergeConfigs tests ====================
test("mergeConfigs does not override local allowed_endpoints if not empty", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "local.endpoint:443",
egress_policy: "audit",
disable_telemetry: false,
disable_sudo: false,
disable_sudo_and_containers: false,
disable_file_monitoring: false,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: ["remote.endpoint:443"],
egress_policy: "block",
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.allowed_endpoints).toBe("local.endpoint:443");
expect(localConfig.egress_policy).toBe("block");
});
test("mergeConfigs overrides disable_sudo_and_containers from remote", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "",
egress_policy: "audit",
disable_telemetry: false,
disable_sudo: false,
disable_sudo_and_containers: false,
disable_file_monitoring: false,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],
disable_sudo_and_containers: true,
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.disable_sudo_and_containers).toBe(true);
});
test("mergeConfigs does not override fields when remote values are undefined", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "",
egress_policy: "block",
disable_telemetry: false,
disable_sudo: true,
disable_sudo_and_containers: true,
disable_file_monitoring: true,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.disable_sudo).toBe(true);
expect(localConfig.disable_sudo_and_containers).toBe(true);
expect(localConfig.disable_file_monitoring).toBe(true);
expect(localConfig.egress_policy).toBe("block");
});

View file

@ -50,6 +50,65 @@ export async function fetchPolicy(
} }
} }
export async function fetchPolicyFromStore(
owner: string,
repo: string,
apiKey: string,
workflow: string,
runId: string,
correlationId: string
): Promise<PolicyResponse | null> {
if (apiKey === "") {
throw new Error("[PolicyStoreFetch]: api-key is empty");
}
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 headers = {};
headers["Authorization"] = `vm-api-key ${apiKey}`;
headers["Source"] = "github-actions";
let response = undefined;
let err = undefined;
let retry = 0;
while (retry < 3) {
try {
console.log(`Attempt: ${retry + 1}`);
response = await httpClient.getJson<PolicyResponse>(
policyEndpoint,
headers
);
break;
} catch (e) {
err = e;
}
retry += 1;
await sleep(1000);
}
if (response === undefined && err !== undefined) {
const error = new Error(`[Policy Store Fetch] ${err}`);
if (err.statusCode !== undefined) {
(error as any).statusCode = err.statusCode;
}
throw error;
}
if (response.statusCode === 404) {
return null;
}
const result = response.result;
if (!result || (!result.egress_policy && (!result.allowed_endpoints || result.allowed_endpoints.length === 0))) {
return null;
}
return result;
}
export function mergeConfigs( export function mergeConfigs(
localConfig: Configuration, localConfig: Configuration,
remoteConfig: PolicyResponse remoteConfig: PolicyResponse

View file

@ -16,7 +16,7 @@ import {
isValidEvent, isValidEvent,
} from "./cache"; } from "./cache";
import { Configuration, PolicyResponse } from "./interfaces"; import { Configuration, PolicyResponse } from "./interfaces";
import { fetchPolicy, mergeConfigs } from "./policy-utils"; import { fetchPolicy, fetchPolicyFromStore, mergeConfigs } from "./policy-utils";
import * as cache from "@actions/cache"; import * as cache from "@actions/cache";
import { getCacheEntry } from "@actions/cache/lib/internal/cacheHttpClient"; import { getCacheEntry } from "@actions/cache/lib/internal/cacheHttpClient";
import * as cacheTwirpClient from "@actions/cache/lib/internal/shared/cacheTwirpClient"; import * as cacheTwirpClient from "@actions/cache/lib/internal/shared/cacheTwirpClient";
@ -87,10 +87,51 @@ interface MonitorResponse {
is_github_hosted: isGithubHosted(), is_github_hosted: isGithubHosted(),
is_debug: core.isDebug(), is_debug: core.isDebug(),
one_time_key: "", one_time_key: "",
api_key: core.getInput("api-key"),
use_policy_store: core.getBooleanInput("use-policy-store"),
}; };
if (confg.api_key !== "") {
core.setSecret(confg.api_key);
}
let policyName = core.getInput("policy"); let policyName = core.getInput("policy");
if (policyName !== "") { if (confg.use_policy_store) {
console.log(`Fetching policy from policy store`);
if (confg.api_key === "") {
core.setFailed("api-key is required when use-policy-store is set to true");
} else {
try {
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
const workflowRef = process.env["GITHUB_WORKFLOW_REF"] || "";
const workflow = workflowRef.replace(/.*\.github\/workflows\//, "").replace(/@.*/, "");
let result: PolicyResponse | null = await fetchPolicyFromStore(
context.repo.owner,
repoName,
confg.api_key,
workflow,
confg.run_id,
confg.correlation_id
);
if (result !== null) {
core.info(`Policy found: ${result.policy_name || "unnamed"}`);
confg = mergeConfigs(confg, result);
} else {
core.info("No policy found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
}
} catch (err) {
core.info(`[!] ${err}`);
if (err.statusCode >= 400 && err.statusCode < 500) {
core.info("Policy not found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
} else {
core.error(`Unexpected error fetching from policy store: ${err}. Falling back to audit mode.`);
confg.egress_policy = "audit";
}
}
}
} else if (policyName !== "") {
console.log(`Fetching policy from API with name: ${policyName}`); console.log(`Fetching policy from API with name: ${policyName}`);
try { try {
let idToken: string = await core.getIDToken(); let idToken: string = await core.getIDToken();
@ -332,7 +373,8 @@ interface MonitorResponse {
return; return;
} }
const configStr = JSON.stringify(confg); const { api_key, use_policy_store, ...agentConfig } = confg;
const configStr = JSON.stringify(agentConfig);
// platform specific // platform specific
let statusFile = ""; let statusFile = "";