1
0
Fork 0
mirror of synced 2026-06-05 09:25:13 +00:00

Compare commits

...

10 commits

Author SHA1 Message Date
Varun Sharma
1b9c34e9f9
bump agent versions: linux v1.8.2, non-tls v0.16.0, windows v1.0.2
Also export CHECKSUMS from checksum.ts and read expected values from it
in checksum.test.ts so tests don't need updating on every version bump.
Mocks the computed hash via crypto.createHash; expected hash comes from
the module itself. Tests now validate branching/platform dispatch
instead of hardcoded hex values. Added darwin and win32 coverage cases.
2026-04-19 09:55:27 -07:00
Varun Sharma
02e8ea858c
write GITHUB_STATE from bravo monitor call so addSummary renders
Post-step common.addSummary() early-returns unless STATE_addSummary is
'true' and STATE_correlation_id is set. The main github-hosted monitor
block writes those alongside monitorStatusCode; the bravo helper was
skipping them, so the job-summary markdown never rendered for
third-party runs.
2026-04-19 08:19:17 -07:00
Varun Sharma
e871c3595b
revert telemetry endpoints to prod
The int switch was only for testing against agent-api's int branch
where the third-party correlation logic lives. Once that logic lands
on main/prod, this is safe.
2026-04-19 08:06:45 -07:00
Varun Sharma
e17c10bfac
guard bravo install on linux only
Bravo tarballs and agent paths are Linux-only (/home/agent, no darwin
or win32 binaries in CHECKSUMS.bravo). Without this guard, a
third-party runner on macOS or Windows would try to download a Linux
tarball and fail. Skip with an info message and return instead.
2026-04-19 08:05:09 -07:00
Varun Sharma
a480e0054e
add unit tests for third-party runner support
- detectThirdPartyRunnerProvider: env-var matrix + precedence ordering
- verifyChecksum: bravo agentType branch, default branch, mismatches
- buildBravoConfig: extracted as pure function; tests lock in the
  shape (no api_key, no customer, is_github_hosted=true, telemetry_url
  forwarded, one_time_key forwarded) that the server-side auth and
  correlation paths depend on

Also capitalize the third-party provider name in the "Detected <X>
runner environment" log line.
2026-04-19 07:50:11 -07:00
Varun Sharma
7b9fcb2585
forward telemetry_url to bravo agent config
Without it the agent falls back to the hardcoded prod default at
config.go:150, sending network/DNS/HTTPS events to prod while process
events (via S3 presigned URL) correctly go to the configured api_url.
Match Jatin's spread behavior so both channels hit the same env.
2026-04-19 07:27:33 -07:00
Varun Sharma
7e1533e993
point telemetry to int env for third-party runner testing
Third-party runner correlation logic in agent-api only exists on the int
branch (commit 3d0c1c7 adding IsThirdPartyHostedRunner/ShouldUseRunnerNameAsCorrelationID).
Prod/main doesn't have it yet, so bravo events uploaded to prod don't
correlate to a run. Match Jatin's fork and point to int endpoints.
2026-04-19 07:23:21 -07:00
Varun Sharma
2f199dceb1
add monitor call for bravo to populate one_time_key
The bravo agent authenticates to the backend using a per-job one_time_key
issued by the /monitor endpoint and stored in DynamoDB keyed by
correlation_id. Without it the presigned-URL request (and all telemetry
endpoints via sendApiRequest) get rejected, so detection events never
upload and insights never appear.

For third-party runners, override correlation_id to RUNNER_NAME before
the monitor call so the key stored in DDB matches the one the bravo
agent will use when requesting presigned URLs. Drop the random api_key
and customer field — when OneTimeKey is present the agent uses
x-one-time-key header, not vm-api-key.
2026-04-19 07:10:45 -07:00
Varun Sharma
fd9b4982b0
forward repo, run_id, private to bravo agent config
Without these the bravo agent can't attribute detection events to the
correct workflow run, so insights don't get generated on the server side.
Matches the set of fields propagated on the original spread-based bravo
config.
2026-04-19 06:46:33 -07:00
Varun Sharma
20c37511ec
add third-party runner support via bravo agent
Detect Depot/Namespace/Warp/Blacksmith runners and install the bravo
agent variant. Bravo install mirrors installAgentForSelfHosted: TLS
gate via isTLSEnabled, hand-picked config literal with random api_key,
correlation_id set to RUNNER_NAME so it matches server-side correlation
derived from job logs (is_github_hosted=true keeps the agent from
overriding correlation_id to customer-hostname).

Bumps agent-ebpf to v1.8.1 and macOS installer to v0.0.5.
2026-04-19 06:29:19 -07:00
15 changed files with 691 additions and 35 deletions

13
dist/index.js vendored
View file

@ -31913,6 +31913,19 @@ function isAgentInstalled(platform) {
function shouldDeployAgentOnSelfHosted(deployOnSelfHostedVm, isContainer, agentAlreadyInstalled) {
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
}
function detectThirdPartyRunnerProvider() {
var _a;
if (process.env["DEPOT_RUNNER"] === "1")
return "depot";
if (process.env["NAMESPACE_GITHUB_RUNTIME"])
return "namespace";
const runnerName = (_a = process.env["RUNNER_NAME"]) !== null && _a !== void 0 ? _a : "";
if (runnerName.startsWith("warp-"))
return "warp";
if (runnerName.startsWith("blacksmith-"))
return "blacksmith";
return null;
}
function utils_getAnnotationLogs(platform) {
switch (platform) {
case "linux":

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

52
dist/post/index.js vendored
View file

@ -31919,6 +31919,19 @@ function isAgentInstalled(platform) {
function shouldDeployAgentOnSelfHosted(deployOnSelfHostedVm, isContainer, agentAlreadyInstalled) {
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
}
function detectThirdPartyRunnerProvider() {
var _a;
if (process.env["DEPOT_RUNNER"] === "1")
return "depot";
if (process.env["NAMESPACE_GITHUB_RUNTIME"])
return "namespace";
const runnerName = (_a = process.env["RUNNER_NAME"]) !== null && _a !== void 0 ? _a : "";
if (runnerName.startsWith("warp-"))
return "warp";
if (runnerName.startsWith("blacksmith-"))
return "blacksmith";
return null;
}
function getAnnotationLogs(platform) {
switch (platform) {
case "linux":
@ -32205,6 +32218,7 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
console.log(`[!] ${ARC_RUNNER_MESSAGE}`);
return;
}
const thirdPartyProvider = detectThirdPartyRunnerProvider();
if (process.env.STATE_selfHosted === "true") {
return;
}
@ -32218,7 +32232,12 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
}
switch (process.platform) {
case "linux":
yield handleLinuxCleanup();
if (thirdPartyProvider) {
yield handleAgentBravoCleanup();
}
else {
yield handleLinuxCleanup();
}
break;
case "win32":
yield handleWindowsCleanup();
@ -32234,6 +32253,37 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
console.log(exception);
}
}))();
function handleAgentBravoCleanup() {
return cleanup_awaiter(this, void 0, void 0, function* () {
external_child_process_.execFileSync("/usr/bin/echo", ["step_policy_jobend"]);
const doneFile = "/home/agent/done.json";
let counter = 0;
while (true) {
if (!external_fs_.existsSync(doneFile)) {
counter++;
if (counter > 10) {
console.log("timed out");
break;
}
yield sleep(1000);
}
else {
console.log(external_fs_.readFileSync(doneFile, "utf-8"));
break;
}
}
const log = "/home/agent/agent.log";
if (external_fs_.existsSync(log)) {
console.log("log:");
console.log(external_fs_.readFileSync(log, "utf-8"));
}
const status = "/home/agent/agent.status";
if (external_fs_.existsSync(status)) {
console.log("status:");
console.log(external_fs_.readFileSync(status, "utf-8"));
}
});
}
function handleLinuxCleanup() {
return cleanup_awaiter(this, void 0, void 0, function* () {
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {

File diff suppressed because one or more lines are too long

175
dist/pre/index.js vendored
View file

@ -84976,6 +84976,7 @@ __nccwpck_require__.r(__webpack_exports__);
// EXPORTS
__nccwpck_require__.d(__webpack_exports__, {
installAgentForBravo: () => (/* binding */ installAgentForBravo),
installAgentForSelfHosted: () => (/* binding */ installAgentForSelfHosted),
sleep: () => (/* binding */ setup_sleep)
});
@ -85037,6 +85038,19 @@ function isAgentInstalled(platform) {
function shouldDeployAgentOnSelfHosted(deployOnSelfHostedVm, isContainer, agentAlreadyInstalled) {
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
}
function detectThirdPartyRunnerProvider() {
var _a;
if (process.env["DEPOT_RUNNER"] === "1")
return "depot";
if (process.env["NAMESPACE_GITHUB_RUNTIME"])
return "namespace";
const runnerName = (_a = process.env["RUNNER_NAME"]) !== null && _a !== void 0 ? _a : "";
if (runnerName.startsWith("warp-"))
return "warp";
if (runnerName.startsWith("blacksmith-"))
return "blacksmith";
return null;
}
function utils_getAnnotationLogs(platform) {
switch (platform) {
case "linux":
@ -85441,19 +85455,23 @@ var external_crypto_ = __nccwpck_require__(6982);
const CHECKSUMS = {
tls: {
amd64: "86d042adcdc03eb1ea50d35d265da47622a6d0aedef9657f84ce1eb7f04d6057",
arm64: "ea1074a2358d50db9a9fe18ae3971b87305cda63f262c494a5f43b25f4e524ce",
amd64: "713c91e921292027dacf446db44bafbc8e36a3f7f51dff664ba681c6e4398a05",
arm64: "2c1eb365d6d9ae4cd4b6632a5f833bcdb7e75d0d9604de3391ff22e4e28e8d42",
},
non_tls: {
amd64: "4aaaeebbe10e619d8ce13e8cc4a1acbafc8f891e8cdd319984480b9ec08407b8", // v0.15.0
amd64: "e38de61e1afd98dd339bb9acce4996183875d482be1638fb198ab02b3e25bbef", // v0.16.0
},
darwin: "797399a3a3f6f9c4c000a02e0d8c7b16499129c9bdc2ad9cf2a10072c10654fb",
bravo: {
amd64: "8d002af0c1c4bb73eaef0f2b641f7aa353cc3f4da36a4e418b69895a2baa922c",
arm64: "1ce74a30d704c2e994246fc809d65af83e3f354aae7b9080b2c2eaee715cf005",
},
darwin: "fe26a1f6af4afe9f1a854d8633832f5d18ab542827003cae445b3a64021d612c",
windows: {
amd64: "e98f8b9cf9ecf6566f1e16a470fbe4aef01610a644fd8203a1bab3ff142186c8", // v1.0.0
amd64: "93f1e5d87c6647e6eca7963d5f4b4bd73107029430f8e6945ffece93007a89f5", // v1.0.2
},
};
// verifyChecksum returns true if checksum is valid
function verifyChecksum(downloadPath, isTLS, variant, platform) {
function verifyChecksum(downloadPath, isTLS, variant, platform, agentType = "default") {
const fileBuffer = external_fs_.readFileSync(downloadPath);
const checksum = external_crypto_.createHash("sha256")
.update(fileBuffer)
@ -85461,9 +85479,14 @@ function verifyChecksum(downloadPath, isTLS, variant, platform) {
let expectedChecksum = "";
switch (platform) {
case "linux":
expectedChecksum = isTLS
? CHECKSUMS["tls"][variant]
: CHECKSUMS["non_tls"][variant];
if (agentType === "bravo") {
expectedChecksum = CHECKSUMS["bravo"][variant];
}
else {
expectedChecksum = isTLS
? CHECKSUMS["tls"][variant]
: CHECKSUMS["non_tls"][variant];
}
break;
case "darwin":
expectedChecksum = CHECKSUMS["darwin"];
@ -85513,14 +85536,14 @@ function installAgent(isTLS, configStr) {
encoding: "utf8",
});
if (isTLS) {
downloadPath = yield tool_cache.downloadTool(`https://github.com/step-security/agent-ebpf/releases/download/v1.8.0/harden-runner_1.8.0_linux_${variant}.tar.gz`, undefined, auth);
downloadPath = yield tool_cache.downloadTool(`https://github.com/step-security/agent-ebpf/releases/download/v1.8.2/harden-runner_1.8.2_linux_${variant}.tar.gz`, undefined, auth);
}
else {
if (variant === "arm64") {
console.log(ARM64_RUNNER_MESSAGE);
return false;
}
downloadPath = yield tool_cache.downloadTool("https://github.com/step-security/agent/releases/download/v0.15.0/agent_0.15.0_linux_amd64.tar.gz", undefined, auth);
downloadPath = yield tool_cache.downloadTool("https://github.com/step-security/agent/releases/download/v0.16.0/agent_0.16.0_linux_amd64.tar.gz", undefined, auth);
}
if (!verifyChecksum(downloadPath, isTLS, variant, "linux")) {
return false;
@ -85542,6 +85565,51 @@ function installAgent(isTLS, configStr) {
return true;
});
}
function installAgentBravo(configStr) {
return install_agent_awaiter(this, void 0, void 0, function* () {
// Note: to avoid github rate limiting
const token = lib_core.getInput("token", { required: true });
const auth = `token ${token}`;
const variant = process.arch === "x64" ? "amd64" : "arm64";
const downloadPath = yield tool_cache.downloadTool(`https://github.com/step-security/agent-ebpf/releases/download/v1.8.2/harden-runner-bravo_1.8.2_linux_${variant}.tar.gz`, undefined, auth);
if (!verifyChecksum(downloadPath, true, variant, "linux", "bravo")) {
return false;
}
const extractPath = yield tool_cache.extractTar(downloadPath);
external_child_process_.execFileSync("cp", [external_path_.join(extractPath, "agent"), "/home/agent/agent"]);
external_child_process_.execSync("chmod +x /home/agent/agent");
external_fs_.writeFileSync("/home/agent/agent.json", configStr);
const logStream = external_fs_.openSync("/home/agent/agent.stdout", "a");
const agentProcess = external_child_process_.spawn("sudo", ["/home/agent/agent"], {
cwd: "/home/agent",
detached: true,
stdio: ["ignore", logStream, logStream],
});
agentProcess.unref();
const agentStatus = "/home/agent/agent.status";
const deadline = Date.now() + 10000;
while (true) {
if (!external_fs_.existsSync(agentStatus)) {
if (Date.now() >= deadline) {
console.log("timed out waiting for bravo agent");
if (external_fs_.existsSync("/home/agent/agent.stdout")) {
console.log(external_fs_.readFileSync("/home/agent/agent.stdout", "utf-8"));
}
if (external_fs_.existsSync("/home/agent/agent.log")) {
console.log(external_fs_.readFileSync("/home/agent/agent.log", "utf-8"));
}
break;
}
yield new Promise((resolve) => setTimeout(resolve, 300));
}
else {
console.log(external_fs_.readFileSync(agentStatus, "utf-8"));
break;
}
}
return true;
});
}
function installMacosAgent(configStr) {
return install_agent_awaiter(this, void 0, void 0, function* () {
const token = lib_core.getInput("token", { required: true });
@ -85557,7 +85625,7 @@ function installMacosAgent(configStr) {
external_fs_.writeFileSync("/opt/step-security/agent.json", configStr);
lib_core.info("✓ Successfully created agent.json at /opt/step-security/agent.json");
// Download installer package
const downloadUrl = "https://github.com/step-security/agent-releases/releases/download/v0.0.4-mac/macos-installer-0.0.4.tar.gz";
const downloadUrl = "https://github.com/step-security/agent-releases/releases/download/v0.0.5-mac/macos-installer-0.0.5.tar.gz";
lib_core.info(`Downloading macOS installer.. : ${downloadUrl}`);
const downloadPath = yield tool_cache.downloadTool(downloadUrl, undefined, auth);
lib_core.info(`✓ Successfully downloaded installer to: ${downloadPath}`);
@ -85622,7 +85690,7 @@ function installWindowsAgent(configStr) {
encoding: "utf8",
});
const agentExePath = external_path_.join(agentDir, "agent.exe");
const downloadPath = yield tool_cache.downloadTool(`https://github.com/step-security/agent-releases/releases/download/v1.0.0-win/harden-runner-agent-windows_1.0.0_windows_amd64.tar.gz`, undefined, auth);
const downloadPath = yield tool_cache.downloadTool(`https://github.com/step-security/agent-releases/releases/download/v1.0.2-win/harden-runner-agent-windows_1.0.2_windows_amd64.tar.gz`, undefined, auth);
// validate the checksum
if (!verifyChecksum(downloadPath, false, variant, process.platform)) {
return false;
@ -85662,6 +85730,27 @@ function installWindowsAgent(configStr) {
});
}
;// CONCATENATED MODULE: ./src/bravo-config.ts
function buildBravoConfig(confg) {
return {
repo: confg.repo,
run_id: confg.run_id,
correlation_id: confg.correlation_id,
working_directory: confg.working_directory,
api_url: confg.api_url,
telemetry_url: confg.telemetry_url,
one_time_key: confg.one_time_key,
allowed_endpoints: confg.allowed_endpoints,
egress_policy: confg.egress_policy,
disable_telemetry: confg.disable_telemetry,
disable_sudo: confg.disable_sudo,
disable_sudo_and_containers: confg.disable_sudo_and_containers,
disable_file_monitoring: confg.disable_file_monitoring,
private: confg.private,
is_github_hosted: true,
};
}
;// CONCATENATED MODULE: ./src/setup.ts
var setup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@ -85703,6 +85792,7 @@ var __rest = (undefined && undefined.__rest) || function (s, e) {
(() => setup_awaiter(void 0, void 0, void 0, function* () {
@ -85896,6 +85986,19 @@ var __rest = (undefined && undefined.__rest) || function (s, e) {
const runnerName = process.env.RUNNER_NAME || "";
lib_core.info(`RUNNER_NAME: ${runnerName}`);
if (!isGithubHosted()) {
const thirdPartyProvider = detectThirdPartyRunnerProvider();
if (thirdPartyProvider) {
const providerLabel = thirdPartyProvider.charAt(0).toUpperCase() + thirdPartyProvider.slice(1);
if (process.platform !== "linux") {
lib_core.info(`Detected ${providerLabel} runner on ${process.platform}. Bravo agent is Linux-only, skipping install.`);
return;
}
lib_core.info(`Detected ${providerLabel} runner environment. Installing agent-bravo.`);
confg.correlation_id = runnerName || confg.correlation_id;
yield callMonitorEndpoint(api_url, confg);
yield installAgentForBravo(github.context.repo.owner, confg);
return;
}
external_fs_.appendFileSync(process.env.GITHUB_STATE, `selfHosted=true${external_os_.EOL}`, {
encoding: "utf8",
});
@ -86041,6 +86144,33 @@ function setup_sleep(ms) {
setTimeout(resolve, ms);
});
}
function callMonitorEndpoint(api_url, confg) {
return setup_awaiter(this, void 0, void 0, function* () {
const _http = new lib.HttpClient();
_http.requestOptions = { socketTimeout: 3 * 1000 };
let statusCode;
let addSummary = "false";
try {
const monitorRequestData = {
correlation_id: confg.correlation_id,
job: process.env["GITHUB_JOB"],
};
const resp = yield _http.postJson(`${api_url}/github/${process.env["GITHUB_REPOSITORY"]}/actions/runs/${process.env["GITHUB_RUN_ID"]}/monitor`, monitorRequestData);
statusCode = resp.statusCode;
if (resp.statusCode === 200 && resp.result) {
console.log(`Runner IP Address: ${resp.result.runner_ip_address}`);
confg.one_time_key = resp.result.one_time_key;
addSummary = resp.result.monitoring_started ? "true" : "false";
}
}
catch (e) {
console.log(`error in connecting to ${api_url}: ${e}`);
}
external_fs_.appendFileSync(process.env.GITHUB_STATE, `monitorStatusCode=${statusCode}${external_os_.EOL}`, { encoding: "utf8" });
external_fs_.appendFileSync(process.env.GITHUB_STATE, `addSummary=${addSummary}${external_os_.EOL}`, { encoding: "utf8" });
external_fs_.appendFileSync(process.env.GITHUB_STATE, `correlation_id=${confg.correlation_id}${external_os_.EOL}`, { encoding: "utf8" });
});
}
function installAgentForSelfHosted(owner, confg) {
return setup_awaiter(this, void 0, void 0, function* () {
try {
@ -86097,6 +86227,25 @@ function installAgentForSelfHosted(owner, confg) {
}
});
}
function installAgentForBravo(owner, confg) {
return setup_awaiter(this, void 0, void 0, function* () {
try {
console.log("Installing Harden Runner bravo agent for third-party runner");
let isTLS = yield isTLSEnabled(owner);
if (!isTLS) {
console.log("TLS is not enabled for this organization. Bravo agent installation skipped.");
return;
}
const bravoConfigStr = JSON.stringify(buildBravoConfig(confg));
external_child_process_.execSync("sudo mkdir -p /home/agent");
chownForFolder(process.env.USER, "/home/agent");
yield installAgentBravo(bravoConfigStr);
}
catch (error) {
console.log(`Failed to install bravo agent: ${error.message}`);
}
});
}
})();

File diff suppressed because one or more lines are too long

81
src/bravo-config.test.ts Normal file
View file

@ -0,0 +1,81 @@
import { buildBravoConfig } from "./bravo-config";
import { Configuration } from "./interfaces";
const base: Configuration = {
repo: "org/repo",
run_id: "123",
correlation_id: "depot-abc",
working_directory: "/w",
api_url: "https://int.api.stepsecurity.io/v1",
telemetry_url: "https://int.app-api.stepsecurity.io/v1",
allowed_endpoints: "github.com:443",
egress_policy: "audit",
disable_telemetry: false,
disable_sudo: false,
disable_sudo_and_containers: false,
disable_file_monitoring: false,
is_github_hosted: false,
private: "true" as unknown as string,
is_debug: false,
one_time_key: "otk-xyz",
api_key: "tenant-key",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
describe("buildBravoConfig", () => {
test("forces is_github_hosted=true so agent honors passed correlation_id", () => {
expect(buildBravoConfig(base).is_github_hosted).toBe(true);
});
test("omits api_key (agent authenticates via one_time_key, not vm-api-key)", () => {
expect(buildBravoConfig(base)).not.toHaveProperty("api_key");
});
test("omits customer (server infers tenant from repo)", () => {
expect(buildBravoConfig(base)).not.toHaveProperty("customer");
});
test("omits use_policy_store (action-side concern, not agent)", () => {
expect(buildBravoConfig(base)).not.toHaveProperty("use_policy_store");
});
test("forwards telemetry_url so network events hit configured env", () => {
expect(buildBravoConfig(base).telemetry_url).toBe(base.telemetry_url);
});
test("forwards one_time_key so agent can auth to presigned URL endpoint", () => {
expect(buildBravoConfig(base).one_time_key).toBe("otk-xyz");
});
test("forwards repo, run_id, correlation_id so server can attribute events", () => {
const cfg = buildBravoConfig(base);
expect(cfg.repo).toBe("org/repo");
expect(cfg.run_id).toBe("123");
expect(cfg.correlation_id).toBe("depot-abc");
});
test("forwards private flag", () => {
expect(buildBravoConfig(base).private).toBe(base.private);
});
test("forwards egress_policy and allowed_endpoints", () => {
const cfg = buildBravoConfig(base);
expect(cfg.egress_policy).toBe("audit");
expect(cfg.allowed_endpoints).toBe("github.com:443");
});
test("forwards disable_* flags", () => {
const cfg = buildBravoConfig({
...base,
disable_telemetry: true,
disable_sudo: true,
disable_sudo_and_containers: true,
disable_file_monitoring: true,
});
expect(cfg.disable_telemetry).toBe(true);
expect(cfg.disable_sudo).toBe(true);
expect(cfg.disable_sudo_and_containers).toBe(true);
expect(cfg.disable_file_monitoring).toBe(true);
});
});

21
src/bravo-config.ts Normal file
View file

@ -0,0 +1,21 @@
import { Configuration } from "./interfaces";
export function buildBravoConfig(confg: Configuration) {
return {
repo: confg.repo,
run_id: confg.run_id,
correlation_id: confg.correlation_id,
working_directory: confg.working_directory,
api_url: confg.api_url,
telemetry_url: confg.telemetry_url,
one_time_key: confg.one_time_key,
allowed_endpoints: confg.allowed_endpoints,
egress_policy: confg.egress_policy,
disable_telemetry: confg.disable_telemetry,
disable_sudo: confg.disable_sudo,
disable_sudo_and_containers: confg.disable_sudo_and_containers,
disable_file_monitoring: confg.disable_file_monitoring,
private: confg.private,
is_github_hosted: true,
};
}

98
src/checksum.test.ts Normal file
View file

@ -0,0 +1,98 @@
import * as fs from "fs";
import * as crypto from "crypto";
import * as core from "@actions/core";
import { verifyChecksum, CHECKSUMS } from "./checksum";
jest.mock("fs", () => ({
...jest.requireActual("fs"),
readFileSync: jest.fn(),
}));
jest.mock("crypto", () => ({
...jest.requireActual("crypto"),
createHash: jest.fn(),
}));
jest.mock("@actions/core");
const mockReadFile = fs.readFileSync as jest.MockedFunction<typeof fs.readFileSync>;
const mockSetFailed = core.setFailed as jest.MockedFunction<typeof core.setFailed>;
const mockCreateHash = crypto.createHash as jest.MockedFunction<typeof crypto.createHash>;
function stubHash(hash: string) {
mockCreateHash.mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: jest.fn().mockReturnValue(hash),
} as unknown as crypto.Hash);
}
const WRONG_HASH = "0".repeat(64);
describe("verifyChecksum", () => {
beforeEach(() => {
jest.clearAllMocks();
mockReadFile.mockReturnValue(Buffer.from("test-payload"));
});
describe("agentType=bravo", () => {
test("passes with matching bravo amd64 checksum", () => {
stubHash(CHECKSUMS.bravo.amd64);
expect(verifyChecksum("/tmp/f", true, "amd64", "linux", "bravo")).toBe(true);
expect(mockSetFailed).not.toHaveBeenCalled();
});
test("passes with matching bravo arm64 checksum", () => {
stubHash(CHECKSUMS.bravo.arm64);
expect(verifyChecksum("/tmp/f", true, "arm64", "linux", "bravo")).toBe(true);
});
test("uses bravo checksum even when isTLS=false", () => {
stubHash(CHECKSUMS.bravo.amd64);
expect(verifyChecksum("/tmp/f", false, "amd64", "linux", "bravo")).toBe(true);
});
test("fails on mismatched bravo checksum", () => {
stubHash(WRONG_HASH);
expect(verifyChecksum("/tmp/f", true, "amd64", "linux", "bravo")).toBe(false);
expect(mockSetFailed).toHaveBeenCalled();
});
});
describe("agentType default (omitted)", () => {
test("uses TLS checksum when isTLS=true", () => {
stubHash(CHECKSUMS.tls.amd64);
expect(verifyChecksum("/tmp/f", true, "amd64", "linux")).toBe(true);
});
test("uses non_tls checksum when isTLS=false", () => {
stubHash(CHECKSUMS.non_tls.amd64);
expect(verifyChecksum("/tmp/f", false, "amd64", "linux")).toBe(true);
});
test("TLS mismatch fails", () => {
stubHash(CHECKSUMS.bravo.amd64);
expect(verifyChecksum("/tmp/f", true, "amd64", "linux")).toBe(false);
expect(mockSetFailed).toHaveBeenCalled();
});
});
describe("darwin", () => {
test("passes with matching darwin checksum", () => {
stubHash(CHECKSUMS.darwin);
expect(verifyChecksum("/tmp/f", false, "", "darwin")).toBe(true);
});
});
describe("win32", () => {
test("passes with matching windows amd64 checksum", () => {
stubHash(CHECKSUMS.windows.amd64);
expect(verifyChecksum("/tmp/f", false, "amd64", "win32")).toBe(true);
});
});
describe("unsupported platform", () => {
test("returns false without calling setFailed", () => {
stubHash(CHECKSUMS.bravo.amd64);
expect(verifyChecksum("/tmp/f", true, "amd64", "freebsd")).toBe(false);
expect(mockSetFailed).not.toHaveBeenCalled();
});
});
});

View file

@ -2,17 +2,21 @@ import * as core from "@actions/core";
import * as crypto from "crypto";
import * as fs from "fs";
const CHECKSUMS = {
export const CHECKSUMS = {
tls: {
amd64: "86d042adcdc03eb1ea50d35d265da47622a6d0aedef9657f84ce1eb7f04d6057", // v1.8.0
arm64: "ea1074a2358d50db9a9fe18ae3971b87305cda63f262c494a5f43b25f4e524ce",
amd64: "713c91e921292027dacf446db44bafbc8e36a3f7f51dff664ba681c6e4398a05", // v1.8.2
arm64: "2c1eb365d6d9ae4cd4b6632a5f833bcdb7e75d0d9604de3391ff22e4e28e8d42",
},
non_tls: {
amd64: "4aaaeebbe10e619d8ce13e8cc4a1acbafc8f891e8cdd319984480b9ec08407b8", // v0.15.0
amd64: "e38de61e1afd98dd339bb9acce4996183875d482be1638fb198ab02b3e25bbef", // v0.16.0
},
darwin: "797399a3a3f6f9c4c000a02e0d8c7b16499129c9bdc2ad9cf2a10072c10654fb", // v0.0.4
bravo: {
amd64: "8d002af0c1c4bb73eaef0f2b641f7aa353cc3f4da36a4e418b69895a2baa922c", // v1.8.2
arm64: "1ce74a30d704c2e994246fc809d65af83e3f354aae7b9080b2c2eaee715cf005",
},
darwin: "fe26a1f6af4afe9f1a854d8633832f5d18ab542827003cae445b3a64021d612c", // v0.0.5
windows: {
amd64: "e98f8b9cf9ecf6566f1e16a470fbe4aef01610a644fd8203a1bab3ff142186c8", // v1.0.0
amd64: "93f1e5d87c6647e6eca7963d5f4b4bd73107029430f8e6945ffece93007a89f5", // v1.0.2
},
};
@ -21,7 +25,8 @@ export function verifyChecksum(
downloadPath: string,
isTLS: boolean,
variant: string,
platform: string
platform: string,
agentType: "default" | "bravo" = "default"
) {
const fileBuffer: Buffer = fs.readFileSync(downloadPath);
const checksum: string = crypto
@ -33,9 +38,13 @@ export function verifyChecksum(
switch (platform) {
case "linux":
expectedChecksum = isTLS
? CHECKSUMS["tls"][variant]
: CHECKSUMS["non_tls"][variant];
if (agentType === "bravo") {
expectedChecksum = CHECKSUMS["bravo"][variant];
} else {
expectedChecksum = isTLS
? CHECKSUMS["tls"][variant]
: CHECKSUMS["non_tls"][variant];
}
break;
case "darwin":
expectedChecksum = CHECKSUMS["darwin"];

View file

@ -6,7 +6,7 @@ import isDocker from "is-docker";
import { isARCRunner } from "./arc-runner";
import { isGithubHosted } from "./tls-inspect";
import { context } from "@actions/github";
import { isPlatformSupported, isAgentInstalled } from "./utils";
import { isPlatformSupported, isAgentInstalled, detectThirdPartyRunnerProvider } from "./utils";
(async () => {
console.log("[harden-runner] post-step");
@ -31,6 +31,8 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
return;
}
const thirdPartyProvider = detectThirdPartyRunnerProvider();
if (process.env.STATE_selfHosted === "true") {
return;
}
@ -49,7 +51,11 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
switch (process.platform) {
case "linux":
await handleLinuxCleanup();
if (thirdPartyProvider) {
await handleAgentBravoCleanup();
} else {
await handleLinuxCleanup();
}
break;
case "win32":
await handleWindowsCleanup();
@ -66,6 +72,38 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
}
})();
async function handleAgentBravoCleanup() {
cp.execFileSync("/usr/bin/echo", ["step_policy_jobend"]);
const doneFile = "/home/agent/done.json";
let counter = 0;
while (true) {
if (!fs.existsSync(doneFile)) {
counter++;
if (counter > 10) {
console.log("timed out");
break;
}
await sleep(1000);
} else {
console.log(fs.readFileSync(doneFile, "utf-8"));
break;
}
}
const log = "/home/agent/agent.log";
if (fs.existsSync(log)) {
console.log("log:");
console.log(fs.readFileSync(log, "utf-8"));
}
const status = "/home/agent/agent.status";
if (fs.existsSync(status)) {
console.log("status:");
console.log(fs.readFileSync(status, "utf-8"));
}
}
async function handleLinuxCleanup() {
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {
return;

View file

@ -26,7 +26,7 @@ export async function installAgent(
if (isTLS) {
downloadPath = await tc.downloadTool(
`https://github.com/step-security/agent-ebpf/releases/download/v1.8.0/harden-runner_1.8.0_linux_${variant}.tar.gz`,
`https://github.com/step-security/agent-ebpf/releases/download/v1.8.2/harden-runner_1.8.2_linux_${variant}.tar.gz`,
undefined,
auth
);
@ -36,7 +36,7 @@ export async function installAgent(
return false;
}
downloadPath = await tc.downloadTool(
"https://github.com/step-security/agent/releases/download/v0.15.0/agent_0.15.0_linux_amd64.tar.gz",
"https://github.com/step-security/agent/releases/download/v0.16.0/agent_0.16.0_linux_amd64.tar.gz",
undefined,
auth
);
@ -69,6 +69,60 @@ export async function installAgent(
return true;
}
export async function installAgentBravo(configStr: string): Promise<boolean> {
// Note: to avoid github rate limiting
const token = core.getInput("token", { required: true });
const auth = `token ${token}`;
const variant = process.arch === "x64" ? "amd64" : "arm64";
const downloadPath = await tc.downloadTool(
`https://github.com/step-security/agent-ebpf/releases/download/v1.8.2/harden-runner-bravo_1.8.2_linux_${variant}.tar.gz`,
undefined,
auth
);
if (!verifyChecksum(downloadPath, true, variant, "linux", "bravo")) {
return false;
}
const extractPath = await tc.extractTar(downloadPath);
cp.execFileSync("cp", [path.join(extractPath, "agent"), "/home/agent/agent"]);
cp.execSync("chmod +x /home/agent/agent");
fs.writeFileSync("/home/agent/agent.json", configStr);
const logStream = fs.openSync("/home/agent/agent.stdout", "a");
const agentProcess = cp.spawn("sudo", ["/home/agent/agent"], {
cwd: "/home/agent",
detached: true,
stdio: ["ignore", logStream, logStream],
});
agentProcess.unref();
const agentStatus = "/home/agent/agent.status";
const deadline = Date.now() + 10000;
while (true) {
if (!fs.existsSync(agentStatus)) {
if (Date.now() >= deadline) {
console.log("timed out waiting for bravo agent");
if (fs.existsSync("/home/agent/agent.stdout")) {
console.log(fs.readFileSync("/home/agent/agent.stdout", "utf-8"));
}
if (fs.existsSync("/home/agent/agent.log")) {
console.log(fs.readFileSync("/home/agent/agent.log", "utf-8"));
}
break;
}
await new Promise((resolve) => setTimeout(resolve, 300));
} else {
console.log(fs.readFileSync(agentStatus, "utf-8"));
break;
}
}
return true;
}
export async function installMacosAgent(configStr: string): Promise<boolean> {
const token = core.getInput("token", { required: true });
const auth = `token ${token}`;
@ -89,7 +143,7 @@ export async function installMacosAgent(configStr: string): Promise<boolean> {
// Download installer package
const downloadUrl =
"https://github.com/step-security/agent-releases/releases/download/v0.0.4-mac/macos-installer-0.0.4.tar.gz";
"https://github.com/step-security/agent-releases/releases/download/v0.0.5-mac/macos-installer-0.0.5.tar.gz";
core.info(`Downloading macOS installer.. : ${downloadUrl}`);
const downloadPath = await tc.downloadTool(downloadUrl, undefined, auth);
core.info(`✓ Successfully downloaded installer to: ${downloadPath}`);
@ -172,7 +226,7 @@ export async function installWindowsAgent(configStr: string): Promise<boolean> {
const agentExePath = path.join(agentDir, "agent.exe");
const downloadPath = await tc.downloadTool(
`https://github.com/step-security/agent-releases/releases/download/v1.0.0-win/harden-runner-agent-windows_1.0.0_windows_amd64.tar.gz`,
`https://github.com/step-security/agent-releases/releases/download/v1.0.2-win/harden-runner-agent-windows_1.0.2_windows_amd64.tar.gz`,
undefined,
auth
);

View file

@ -33,11 +33,13 @@ import {
import { isGithubHosted, isTLSEnabled } from "./tls-inspect";
import {
installAgent,
installAgentBravo,
installMacosAgent,
installWindowsAgent,
} from "./install-agent";
import { chownForFolder, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted } from "./utils";
import { chownForFolder, detectThirdPartyRunnerProvider, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted } from "./utils";
import { buildBravoConfig } from "./bravo-config";
interface MonitorResponse {
runner_ip_address?: string;
@ -289,6 +291,20 @@ interface MonitorResponse {
const runnerName = process.env.RUNNER_NAME || "";
core.info(`RUNNER_NAME: ${runnerName}`);
if (!isGithubHosted()) {
const thirdPartyProvider = detectThirdPartyRunnerProvider();
if (thirdPartyProvider) {
const providerLabel = thirdPartyProvider.charAt(0).toUpperCase() + thirdPartyProvider.slice(1);
if (process.platform !== "linux") {
core.info(`Detected ${providerLabel} runner on ${process.platform}. Bravo agent is Linux-only, skipping install.`);
return;
}
core.info(`Detected ${providerLabel} runner environment. Installing agent-bravo.`);
confg.correlation_id = runnerName || confg.correlation_id;
await callMonitorEndpoint(api_url, confg);
await installAgentForBravo(context.repo.owner, confg);
return;
}
fs.appendFileSync(process.env.GITHUB_STATE, `selfHosted=true${EOL}`, {
encoding: "utf8",
});
@ -470,6 +486,34 @@ export function sleep(ms: number) {
});
}
async function callMonitorEndpoint(api_url: string, confg: Configuration) {
const _http = new httpm.HttpClient();
_http.requestOptions = { socketTimeout: 3 * 1000 };
let statusCode: number | undefined;
let addSummary = "false";
try {
const monitorRequestData = {
correlation_id: confg.correlation_id,
job: process.env["GITHUB_JOB"],
};
const resp = await _http.postJson<MonitorResponse>(
`${api_url}/github/${process.env["GITHUB_REPOSITORY"]}/actions/runs/${process.env["GITHUB_RUN_ID"]}/monitor`,
monitorRequestData
);
statusCode = resp.statusCode;
if (resp.statusCode === 200 && resp.result) {
console.log(`Runner IP Address: ${resp.result.runner_ip_address}`);
confg.one_time_key = resp.result.one_time_key;
addSummary = resp.result.monitoring_started ? "true" : "false";
}
} catch (e) {
console.log(`error in connecting to ${api_url}: ${e}`);
}
fs.appendFileSync(process.env.GITHUB_STATE, `monitorStatusCode=${statusCode}${EOL}`, { encoding: "utf8" });
fs.appendFileSync(process.env.GITHUB_STATE, `addSummary=${addSummary}${EOL}`, { encoding: "utf8" });
fs.appendFileSync(process.env.GITHUB_STATE, `correlation_id=${confg.correlation_id}${EOL}`, { encoding: "utf8" });
}
export async function installAgentForSelfHosted(owner: string, confg: Configuration) {
try {
console.log("Installing Harden Runner agent for self-hosted runner");
@ -528,3 +572,25 @@ export async function installAgentForSelfHosted(owner: string, confg: Configurat
console.log(`Failed to install agent for self-hosted runner: ${error.message}`);
}
}
export async function installAgentForBravo(owner: string, confg: Configuration) {
try {
console.log("Installing Harden Runner bravo agent for third-party runner");
let isTLS = await isTLSEnabled(owner);
if (!isTLS) {
console.log("TLS is not enabled for this organization. Bravo agent installation skipped.");
return;
}
const bravoConfigStr = JSON.stringify(buildBravoConfig(confg));
cp.execSync("sudo mkdir -p /home/agent");
chownForFolder(process.env.USER, "/home/agent");
await installAgentBravo(bravoConfigStr);
} catch (error) {
console.log(`Failed to install bravo agent: ${error.message}`);
}
}

View file

@ -1,4 +1,4 @@
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs } from "./utils";
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs, detectThirdPartyRunnerProvider } from "./utils";
import * as fs from "fs";
jest.mock("fs", () => ({
@ -90,3 +90,69 @@ describe("getAnnotationLogs", () => {
expect(() => getAnnotationLogs("freebsd" as NodeJS.Platform)).toThrow("platform not supported");
});
});
describe("detectThirdPartyRunnerProvider", () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.DEPOT_RUNNER;
delete process.env.NAMESPACE_GITHUB_RUNTIME;
delete process.env.RUNNER_NAME;
});
afterAll(() => {
process.env = originalEnv;
});
test("returns depot when DEPOT_RUNNER=1", () => {
process.env.DEPOT_RUNNER = "1";
expect(detectThirdPartyRunnerProvider()).toBe("depot");
});
test("returns null when DEPOT_RUNNER=0", () => {
process.env.DEPOT_RUNNER = "0";
expect(detectThirdPartyRunnerProvider()).toBeNull();
});
test("returns namespace when NAMESPACE_GITHUB_RUNTIME is set", () => {
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
expect(detectThirdPartyRunnerProvider()).toBe("namespace");
});
test("returns warp for RUNNER_NAME prefix warp-", () => {
process.env.RUNNER_NAME = "warp-4x-x64-abc";
expect(detectThirdPartyRunnerProvider()).toBe("warp");
});
test("returns blacksmith for RUNNER_NAME prefix blacksmith-", () => {
process.env.RUNNER_NAME = "blacksmith-01kpj-4vcpu";
expect(detectThirdPartyRunnerProvider()).toBe("blacksmith");
});
test("returns null when no env vars match", () => {
expect(detectThirdPartyRunnerProvider()).toBeNull();
});
test("returns null for a non-matching RUNNER_NAME", () => {
process.env.RUNNER_NAME = "GitHub Actions 1";
expect(detectThirdPartyRunnerProvider()).toBeNull();
});
test("depot takes precedence over namespace", () => {
process.env.DEPOT_RUNNER = "1";
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
expect(detectThirdPartyRunnerProvider()).toBe("depot");
});
test("namespace takes precedence over warp runner name prefix", () => {
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
process.env.RUNNER_NAME = "warp-x";
expect(detectThirdPartyRunnerProvider()).toBe("namespace");
});
test("warp takes precedence over blacksmith when both prefixes seen (warp wins on name check order)", () => {
process.env.RUNNER_NAME = "warp-x";
expect(detectThirdPartyRunnerProvider()).toBe("warp");
});
});

View file

@ -40,6 +40,17 @@ export function shouldDeployAgentOnSelfHosted(
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
}
export type ThirdPartyRunnerProvider = "depot" | "namespace" | "warp" | "blacksmith";
export function detectThirdPartyRunnerProvider(): ThirdPartyRunnerProvider | null {
if (process.env["DEPOT_RUNNER"] === "1") return "depot";
if (process.env["NAMESPACE_GITHUB_RUNTIME"]) return "namespace";
const runnerName = process.env["RUNNER_NAME"] ?? "";
if (runnerName.startsWith("warp-")) return "warp";
if (runnerName.startsWith("blacksmith-")) return "blacksmith";
return null;
}
export function getAnnotationLogs(platform: NodeJS.Platform) {
switch (platform) {
case "linux":