From 3559266854707a20060c6df28f4894e6576fa2e9 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:50:24 +0530 Subject: [PATCH] added maco agent installer --- src/checksum.ts | 9 ++ src/cleanup.ts | 231 +++++++++++++++++++++++++++---------------- src/common.ts | 20 ++-- src/install-agent.ts | 84 +++++++++++++++- src/setup.ts | 115 ++++++++++++--------- 5 files changed, 323 insertions(+), 136 deletions(-) diff --git a/src/checksum.ts b/src/checksum.ts index c7574e4..c0b316c 100644 --- a/src/checksum.ts +++ b/src/checksum.ts @@ -39,3 +39,12 @@ export function verifyChecksum( core.debug("Checksum verification passed."); } + +export function calculateSha256(filePath: string): string { + const fileBuffer: Buffer = fs.readFileSync(filePath); + const checksum: string = crypto + .createHash("sha256") + .update(fileBuffer) + .digest("hex"); + return checksum; +} diff --git a/src/cleanup.ts b/src/cleanup.ts index bcaf1a9..beaa7c4 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -14,9 +14,14 @@ import { context } from "@actions/github"; return; } - if (process.platform !== "linux") { - console.log(common.UBUNTU_MESSAGE); - return; + let platform = process.platform; + switch (platform) { + case "linux": + case "darwin": + break; + default: + console.log(common.UBUNTU_MESSAGE); + return; } if (isGithubHosted() && isDocker()) { console.log(common.CONTAINER_MESSAGE); @@ -28,91 +33,149 @@ import { context } from "@actions/github"; return; } - if (process.env.STATE_selfHosted === "true") { - return; - } + switch (platform) { + case "darwin": + { + fs.writeFileSync( + "/opt/step-security/post_event.json", + JSON.stringify({ event: "post" }) + ); - if (process.env.STATE_customVMImage === "true") { - return; - } + let macDone = "/opt/step-security/done.json"; + let counter = 0; + while (true) { + if (!fs.existsSync(macDone)) { + counter++; + if (counter > 10) { + console.log("timed out"); - if (process.env.STATE_isTLS === "false" && process.arch === "arm64") { - return; - } - - if ( - String(process.env.STATE_monitorStatusCode) === - common.STATUS_HARDEN_RUNNER_UNAVAILABLE - ) { - console.log(common.HARDEN_RUNNER_UNAVAILABLE_MESSAGE); - return; - } - - if (isGithubHosted() && fs.existsSync("/home/agent/post_event.json")) { - console.log("Post step already executed, skipping"); - return; - } - - fs.writeFileSync( - "/home/agent/post_event.json", - JSON.stringify({ event: "post" }) - ); - - 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); - } // The file *does* exist - else { - break; - } - } - - const log = "/home/agent/agent.log"; - if (fs.existsSync(log)) { - console.log("log:"); - var content = fs.readFileSync(log, "utf-8"); - console.log(content); - } - - const daemonLog = "/home/agent/daemon.log"; - if (fs.existsSync(daemonLog)) { - console.log("daemonLog:"); - var content = fs.readFileSync(daemonLog, "utf-8"); - console.log(content); - } - - var status = "/home/agent/agent.status"; - if (fs.existsSync(status)) { - console.log("status:"); - var content = fs.readFileSync(status, "utf-8"); - console.log(content); - } - - var disable_sudo = process.env.STATE_disableSudo; - var disable_sudo_and_containers = process.env.STATE_disableSudoAndContainers; - - if (disable_sudo !== "true" && disable_sudo_and_containers !== "true") { - try { - var journalLog = cp.execSync( - "sudo journalctl -u agent.service --lines=1000", - { - encoding: "utf8", - maxBuffer: 1024 * 1024 * 10, // 10MB buffer + break; + } + await sleep(1000); + } // The file *does* exist + else { + break; + } } + + let macAgenLog = "/opt/step-security/agent.log"; + if (fs.existsSync(macAgenLog)) { + console.log("macAgenLog:"); + var content = fs.readFileSync(macAgenLog, "utf-8"); + console.log(content); + } else { + console.log("😭 macos agent.log file not found"); + } + + // Capture system log stream for harden-runner subsystem + try { + console.log("\nSystem log stream for io.stepsecurity.harden-runner:"); + const logStreamOutput = cp.execSync( + "log show --predicate 'subsystem == \"io.stepsecurity.harden-runner\"' --info --last 10m", + { + encoding: "utf8", + maxBuffer: 1024 * 1024 * 10, // 10MB buffer + timeout: 10000, // 30 second timeout + } + ); + console.log(logStreamOutput); + } catch (error) { + console.log( + "Warning: Could not fetch system log stream:", + error.message + ); + } + } + break; + + case "linux": + if (process.env.STATE_selfHosted === "true") { + return; + } + + if (process.env.STATE_customVMImage === "true") { + return; + } + + if (process.env.STATE_isTLS === "false" && process.arch === "arm64") { + return; + } + + if ( + String(process.env.STATE_monitorStatusCode) === + common.STATUS_HARDEN_RUNNER_UNAVAILABLE + ) { + console.log(common.HARDEN_RUNNER_UNAVAILABLE_MESSAGE); + return; + } + + if (isGithubHosted() && fs.existsSync("/home/agent/post_event.json")) { + console.log("Post step already executed, skipping"); + return; + } + + fs.writeFileSync( + "/home/agent/post_event.json", + JSON.stringify({ event: "post" }) ); - console.log("agent.service log:"); - console.log(journalLog); - } catch (error) { - console.log("Warning: Could not fetch service logs:", error.message); - } + + 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); + } // The file *does* exist + else { + break; + } + } + + const log = "/home/agent/agent.log"; + if (fs.existsSync(log)) { + console.log("log:"); + var content = fs.readFileSync(log, "utf-8"); + console.log(content); + } + + const daemonLog = "/home/agent/daemon.log"; + if (fs.existsSync(daemonLog)) { + console.log("daemonLog:"); + var content = fs.readFileSync(daemonLog, "utf-8"); + console.log(content); + } + + var status = "/home/agent/agent.status"; + if (fs.existsSync(status)) { + console.log("status:"); + var content = fs.readFileSync(status, "utf-8"); + console.log(content); + } + + var disable_sudo = process.env.STATE_disableSudo; + var disable_sudo_and_containers = + process.env.STATE_disableSudoAndContainers; + + if (disable_sudo !== "true" && disable_sudo_and_containers !== "true") { + try { + var journalLog = cp.execSync( + "sudo journalctl -u agent.service --lines=1000", + { + encoding: "utf8", + maxBuffer: 1024 * 1024 * 10, // 10MB buffer + } + ); + console.log("agent.service log:"); + console.log(journalLog); + } catch (error) { + console.log("Warning: Could not fetch service logs:", error.message); + } + } } try { diff --git a/src/common.ts b/src/common.ts index 3d32e5f..2e5f8be 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,5 +1,6 @@ import * as core from "@actions/core"; import * as fs from "fs"; +import * as cp from "child_process"; import { STEPSECURITY_API_URL, STEPSECURITY_WEB_URL } from "./configs"; export function printInfo(web_url) { @@ -64,7 +65,6 @@ export async function addSummary() { return; } - let needsSubscription = false; try { let data = fs.readFileSync("/home/agent/annotation.log", "utf8"); @@ -97,23 +97,25 @@ export async function addSummary() { // Extract owner and repo from GITHUB_REPOSITORY (format: owner/repo) const [owner, repo] = process.env["GITHUB_REPOSITORY"]?.split("/") || []; const run_id = process.env["GITHUB_RUN_ID"]; - + if (!owner || !repo || !run_id || !correlation_id) { return; } // Fetch job summary from API const apiUrl = `${STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/runs/${run_id}/correlation/${correlation_id}/job-markdown-summary`; - + try { const response = await fetch(apiUrl); if (!response.ok) { - console.error(`Failed to fetch job summary: ${response.status} ${response.statusText}`); + console.error( + `Failed to fetch job summary: ${response.status} ${response.statusText}` + ); return; } - + const markdownSummary = await response.text(); - + // Render the markdown summary using core.summary.addRaw await core.summary.addRaw(markdownSummary).write(); return; @@ -123,6 +125,12 @@ export async function addSummary() { } } +export function chownForFolder(newOwner: string, target: string) { + let cmd = "sudo"; + let args = ["chown", "-R", newOwner, target]; + cp.execFileSync(cmd, args); +} + export const STATUS_HARDEN_RUNNER_UNAVAILABLE = "409"; export const CONTAINER_MESSAGE = diff --git a/src/install-agent.ts b/src/install-agent.ts index 0ce67c7..c8a1fcd 100644 --- a/src/install-agent.ts +++ b/src/install-agent.ts @@ -3,9 +3,9 @@ import * as core from "@actions/core"; import * as cp from "child_process"; import * as path from "path"; import * as fs from "fs"; -import { verifyChecksum } from "./checksum"; +import { verifyChecksum, calculateSha256 } from "./checksum"; import { EOL } from "os"; -import { ARM64_RUNNER_MESSAGE } from "./common"; +import { ARM64_RUNNER_MESSAGE, chownForFolder } from "./common"; export async function installAgent( isTLS: boolean, @@ -65,3 +65,83 @@ export async function installAgent( cp.execSync("sudo service agent start", { timeout: 15000 }); return true; } + +export async function installMacosAgent(confgStr: string): Promise { + const token = core.getInput("token", { required: true }); + const auth = `token ${token}`; + + try { + // Create working directory + core.info("Creating /opt/step-security directory..."); + cp.execSync("sudo mkdir -p /opt/step-security"); + chownForFolder(process.env.USER, "/opt/step-security"); + core.info("✓ Successfully created /opt/step-security directory"); + + // Create agent configuration file + core.info("Creating agent.json"); + fs.writeFileSync("/opt/step-security/agent.json", confgStr); + 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/v1.0.0-int/macos-installer-0.0.1-rc4.tar.gz"; + core.info(`Downloading macOS installer.. : ${downloadUrl}`); + const downloadPath = await tc.downloadTool(downloadUrl); + core.info(`✓ Successfully downloaded installer to: ${downloadPath}`); + + // Calculate and print SHA256 checksum + core.info("Calculating SHA256 checksum of downloaded tar file..."); + const sha256sum = calculateSha256(downloadPath); + core.info(`SHA256: ${sha256sum}`); + + // Extract installer package + core.info("Extracting installer..."); + const extractPath = await tc.extractTar(downloadPath); + core.info(`✓ Successfully extracted installer to: ${extractPath}`); + + // Copy Installer binary to /opt/step-security + const installerSourcePath = path.join(extractPath, "Installer"); + core.info( + `Copying Installer from ${installerSourcePath} to /opt/step-security...` + ); + cp.execSync(`cp "${installerSourcePath}" /opt/step-security/`); + core.info("✓ Successfully copied Installer to /opt/step-security"); + + const installerBinaryPath = "/opt/step-security/Installer"; + + // Verify installer binary exists + if (!fs.existsSync(installerBinaryPath)) { + throw new Error( + "Installer binary not found at /opt/step-security/Installer" + ); + } + core.info("✓ Installer binary verified"); + + // Make installer executable + core.info("Making installer executable..."); + cp.execSync(`chmod +x "${installerBinaryPath}"`); + core.info("✓ Installer is now executable"); + + // Run installer + core.info("Running installer..."); + cp.execSync( + `sudo "${installerBinaryPath}" -workdir /opt/step-security >> /opt/step-security/agent.log 2>&1`, + { + shell: "/bin/bash", + timeout: 60000, // 60 second timeout + } + ); + core.info("✓ Installer completed successfully"); + + core.info("✅ macOS agent installation (method 2) completed successfully"); + return true; + } catch (error) { + core.error(`❌ Failed to install macOS agent: ${error}`); + if (error instanceof Error && error.stack) { + core.debug(error.stack); + } + return false; + } +} diff --git a/src/setup.ts b/src/setup.ts index 774dcb7..b2a0187 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -27,7 +27,7 @@ import * as utils from "@actions/cache/lib/internal/cacheUtils"; import { isARCRunner, sendAllowedEndpoints } from "./arc-runner"; import { STEPSECURITY_API_URL, STEPSECURITY_WEB_URL } from "./configs"; import { isGithubHosted, isTLSEnabled } from "./tls-inspect"; -import { installAgent } from "./install-agent"; +import { installAgent, installMacosAgent } from "./install-agent"; interface MonitorResponse { runner_ip_address?: string; @@ -39,16 +39,25 @@ interface MonitorResponse { try { console.log("[harden-runner] pre-step"); - const customProperties = context?.payload?.repository?.custom_properties || {}; + const customProperties = + context?.payload?.repository?.custom_properties || {}; if (customProperties["skip-harden-runner"] === "true") { - console.log("Skipping harden-runner: custom property 'skip-harden-runner' is set to 'true'"); + console.log( + "Skipping harden-runner: custom property 'skip-harden-runner' is set to 'true'" + ); return; } - if (process.platform !== "linux") { - console.log(common.UBUNTU_MESSAGE); - return; + let platform = process.platform; + switch (platform) { + case "linux": + case "darwin": + break; + default: + console.log(common.UBUNTU_MESSAGE); + return; } + if (isGithubHosted() && isDocker()) { console.log(common.CONTAINER_MESSAGE); return; @@ -92,15 +101,22 @@ interface MonitorResponse { } catch (err) { core.info(`[!] ${err}`); // Only fail the job if ID token is not available - if (err.message && err.message.includes('Unable to get ACTIONS_ID_TOKEN_REQUEST')) { - core.setFailed('Policy store requires id-token write permission as it uses OIDC to fetch the policy from StepSecurity API. Please add "id-token: write" to your job permissions.'); + if ( + err.message && + err.message.includes("Unable to get ACTIONS_ID_TOKEN_REQUEST") + ) { + core.setFailed( + 'Policy store requires id-token write permission as it uses OIDC to fetch the policy from StepSecurity API. Please add "id-token: write" to your job permissions.' + ); } else { // Handle different HTTP status codes if (err.statusCode >= 400 && err.statusCode < 500) { - core.error('Policy not found'); + core.error("Policy not found"); } else { - core.error(`Unexpected error occurred: ${err}. Falling back to egress policy audit`); - confg.egress_policy = 'audit'; + core.error( + `Unexpected error occurred: ${err}. Falling back to egress policy audit` + ); + confg.egress_policy = "audit"; } } } @@ -249,12 +265,17 @@ interface MonitorResponse { return; } - if (isGithubHosted() && process.env.STEP_SECURITY_HARDEN_RUNNER === "true") { + if ( + isGithubHosted() && + process.env.STEP_SECURITY_HARDEN_RUNNER === "true" + ) { fs.appendFileSync(process.env.GITHUB_STATE, `customVMImage=true${EOL}`, { encoding: "utf8", }); - core.info("This job is running on a custom VM image with Harden Runner installed."); + core.info( + "This job is running on a custom VM image with Harden Runner installed." + ); if (confg.egress_policy === "block") { sendAllowedEndpoints(confg.allowed_endpoints); @@ -322,39 +343,51 @@ interface MonitorResponse { } const confgStr = JSON.stringify(confg); - cp.execSync("sudo mkdir -p /home/agent"); - chownForFolder(process.env.USER, "/home/agent"); - let isTLS = await isTLSEnabled(context.repo.owner); + switch (platform) { + case "linux": + cp.execSync("sudo mkdir -p /home/agent"); + common.chownForFolder(process.env.USER, "/home/agent"); - const agentInstalled = await installAgent(isTLS, confgStr); + let isTLS = await isTLSEnabled(context.repo.owner); - if (agentInstalled) { - // Check that the file exists locally - var statusFile = "/home/agent/agent.status"; - var logFile = "/home/agent/agent.log"; - var counter = 0; - while (true) { - if (!fs.existsSync(statusFile)) { - counter++; - if (counter > 30) { - console.log("timed out"); - if (fs.existsSync(logFile)) { - var content = fs.readFileSync(logFile, "utf-8"); + const agentInstalled = await installAgent(isTLS, confgStr); + + if (agentInstalled) { + // Check that the file exists locally + var statusFile = "/home/agent/agent.status"; + var logFile = "/home/agent/agent.log"; + var counter = 0; + while (true) { + if (!fs.existsSync(statusFile)) { + counter++; + if (counter > 30) { + console.log("timed out"); + if (fs.existsSync(logFile)) { + var content = fs.readFileSync(logFile, "utf-8"); + console.log(content); + } + break; + } + await sleep(300); + } // The file *does* exist + else { + // Read the file + var content = fs.readFileSync(statusFile, "utf-8"); console.log(content); + break; } - break; } - await sleep(300); - } // The file *does* exist - else { - // Read the file - var content = fs.readFileSync(statusFile, "utf-8"); - console.log(content); - break; } - } + + case "darwin": + const installed = await installMacosAgent(confgStr); + if (!installed) { + core.warning("😭 macos agent installation failed"); + return; + } } + } catch (error) { core.setFailed(error.message); } @@ -367,9 +400,3 @@ export function sleep(ms: number) { setTimeout(resolve, ms); }); } - -function chownForFolder(newOwner: string, target: string) { - let cmd = "sudo"; - let args = ["chown", "-R", newOwner, target]; - cp.execFileSync(cmd, args); -}