Feature/policy store (#656)
This commit is contained in:
parent
fe10465874
commit
f808768d15
7 changed files with 521 additions and 7 deletions
10
action.yml
10
action.yml
|
|
@ -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
96
dist/pre/index.js
vendored
|
|
@ -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 = "";
|
||||||
|
|
|
||||||
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
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
48
src/setup.ts
48
src/setup.ts
|
|
@ -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 = "";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue