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

Merge branch 'main' into fix/ubuntu-slim-user-env

This commit is contained in:
Nikolai Emil Damm 2026-04-25 17:29:39 +02:00 committed by GitHub
commit 4e0504ee08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1068 additions and 102 deletions

View file

@ -22,6 +22,8 @@ Traditional security monitoring and EDR solutions are ineffective for CI/CD runn
StepSecurity Harden-Runner addresses this gap by providing security monitoring tailored for CI/CD runners, with support for Linux, Windows, and macOS runners. This approach brings CI/CD runners under the same level of security scrutiny as other critical systems, addressing a significant gap in the software supply chain.
### Harden-Runner: Security Incidents Detected
- [Harden-Runner Detected the Compromised axios npm Package Dropping a Remote Access Trojan](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan) ([backstage/backstage#33693](https://github.com/backstage/backstage/issues/33693), [block/elasticgraph#1103](https://github.com/block/elasticgraph/issues/1103))
- [Harden-Runner Detected the Trivy Compromise with Malicious v0.69.4 Release](https://www.stepsecurity.io/blog/trivy-compromised-a-second-time---malicious-v0-69-4-release) ([k8gb-io/k8gb#2294](https://github.com/k8gb-io/k8gb/issues/2294))
- [Harden-Runner Detected the tj-actions/changed-files compromise](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) ([CVE-2025-30066](https://github.com/advisories/GHSA-mrrh-fwg8-r2c3))
- [Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCFs Backstage Repository](https://www.stepsecurity.io/blog/how-harden-runner-detected-the-sha1-hulud-supply-chain-attack-in-cncfs-backstage-repository)
- [Harden-Runner Detected the NX Build System compromise](https://www.stepsecurity.io/blog/supply-chain-security-alert-popular-nx-build-system-package-compromised-with-data-stealing-malware)
@ -32,7 +34,7 @@ StepSecurity Harden-Runner addresses this gap by providing security monitoring t
- [Harden-Runner Flagged an Anomalous Outbound Call, Leading to a Docker Documentation Update](https://www.stepsecurity.io/blog/harden-runner-flags-anomalous-outbound-call-leading-to-docker-documentation-update)
### See It in Action
Harden-Runner secures over **18 million CI/CD workflow runs every week**, protecting thousands of pipelines, including those from popular open-source projects by **Microsoft, Google, and CISA**. See how top projects are using Harden-Runner and explore the insights:
Harden-Runner secures over **25 million CI/CD workflow runs every week**, protecting thousands of pipelines, including those from popular open-source projects by **Microsoft, Google, and CISA**. See how top projects are using Harden-Runner and explore the insights:
➡️ [Who's using Harden-Runner?](https://docs.stepsecurity.io/whos-using-harden-runner)
## Quick Links
@ -70,7 +72,7 @@ To integrate Harden-Runner, follow these steps:
```yaml
steps:
- name: Harden Runner
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
@ -149,11 +151,11 @@ Harden-Runner is trusted by over 11,000 leading open-source projects and enterpr
### Enterprise Case Studies
- [How Mercari Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/mercari)
- [How Omnissa Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/omnissa)
- [Chainguard Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/chainguard)
- [How Coveo Strengthened GitHub Actions Security with StepSecurity](https://www.stepsecurity.io/case-studies/coveo)
- [Hashgraph Achieves Comprehensive CI/CD Security Without Compromising Development Speed](https://www.stepsecurity.io/case-studies/hashgraph)
- [Chainguard Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/chainguard)
- [Kapiche secures their GitHub Actions software supply chain with Harden-Runner](https://www.stepsecurity.io/case-studies/kapiche)
- [Arcjet Enhances CI/CD Security with Harden-Runner](https://www.stepsecurity.io/case-studies/arcjet)
---
@ -161,7 +163,7 @@ Harden-Runner is trusted by over 11,000 leading open-source projects and enterpr
Harden-Runner is designed to work seamlessly across a variety of runner environments, providing consistent security insights and protections regardless of where your workflows execute. For self-hosted runners, audit mode is deployed directly to the runner infrastructure without requiring any changes to your existing workflows. For more details, refer to the [official documentation](https://docs.stepsecurity.io/harden-runner).
| Environment Type | Compatibility | Audit Mode Deployment | Workflow Changes for Audit Mode |
| Environment Type | Compatibility | Audit Mode Deployment | Workflow Changes for Audit/Block Mode |
|------------------|---------------|--------------------------|-------------------|
| GitHub-hosted runners (Linux) | ✅ Full support | Add Harden-Runner Action to workflow | Yes |
| GitHub-hosted runners (Windows, macOS) | ✅ Audit mode only | Add Harden-Runner Action to workflow | Yes |

View file

@ -40,6 +40,10 @@ inputs:
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"
deploy-on-self-hosted-vm:
description: "Set to true to deploy the Harden Runner agent directly on a self-hosted runner VM (Linux only). The recommended approach for self-hosted VMs is to bake the agent into the VM image; see docs.stepsecurity.io. Use this option only if baking is not possible, and only for ephemeral runners."
required: false
default: "false"
branding:
icon: "check-square"

16
dist/index.js vendored
View file

@ -31910,6 +31910,22 @@ function isAgentInstalled(platform) {
return false;
}
}
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

55
dist/post/index.js vendored
View file

@ -31916,6 +31916,22 @@ function isAgentInstalled(platform) {
return false;
}
}
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":
@ -32202,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;
}
@ -32215,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();
@ -32231,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

283
dist/pre/index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

143
package-lock.json generated
View file

@ -2852,9 +2852,10 @@
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -3765,21 +3766,9 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-builder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/fast-xml-parser": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
"funding": [
{
"type": "github",
@ -3788,8 +3777,24 @@
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
"path-expression-matcher": "^1.1.3"
}
},
"node_modules/fast-xml-parser": {
"version": "5.5.11",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.11.tgz",
"integrity": "sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.1.4",
"path-expression-matcher": "^1.4.0",
"strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
@ -3886,10 +3891,11 @@
}
},
"node_modules/flatted": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
"node_modules/form-data": {
"version": "2.5.5",
@ -6079,9 +6085,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
@ -6448,6 +6454,21 @@
"node": ">=8"
}
},
"node_modules/path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -6487,10 +6508,11 @@
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@ -6966,9 +6988,9 @@
}
},
"node_modules/strnum": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
"funding": [
{
"type": "github",
@ -9700,9 +9722,9 @@
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -10371,17 +10393,21 @@
"dev": true
},
"fast-xml-builder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ=="
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
"requires": {
"path-expression-matcher": "^1.1.3"
}
},
"fast-xml-parser": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
"version": "5.5.11",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.11.tgz",
"integrity": "sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==",
"requires": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
"fast-xml-builder": "^1.1.4",
"path-expression-matcher": "^1.4.0",
"strnum": "^2.2.3"
}
},
"fastq": {
@ -10450,9 +10476,9 @@
}
},
"flatted": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true
},
"form-data": {
@ -12089,9 +12115,9 @@
}
},
"lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true
},
"lodash.memoize": {
@ -12358,6 +12384,11 @@
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -12388,9 +12419,9 @@
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true
},
"pirates": {
@ -12717,9 +12748,9 @@
"dev": true
},
"strnum": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="
},
"supports-color": {
"version": "7.2.0",

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: "d4b80f15758bb950787000e802cc58a565919a8cb9ecf405777b304ef42911fe", // v1.7.15
arm64: "3c224ea1da1776d1ba9f70b8dd8f0d8432230a7c2d464bca84bbdee8b7d46f6c",
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");
@ -36,6 +36,8 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
return;
}
const thirdPartyProvider = detectThirdPartyRunnerProvider();
if (process.env.STATE_selfHosted === "true") {
return;
}
@ -54,7 +56,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();
@ -71,6 +77,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.7.15/harden-runner_1.7.15_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

@ -17,6 +17,7 @@ export interface Configuration {
one_time_key: string;
api_key: string;
use_policy_store: boolean;
deploy_on_self_hosted_vm: boolean;
}
export interface PolicyResponse {

View file

@ -45,6 +45,7 @@ test("merge configs", async () => {
one_time_key: "",
api_key: "",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
let policyResponse: PolicyResponse = {
owner: "h0x0er",
@ -75,6 +76,7 @@ test("merge configs", async () => {
one_time_key: "",
api_key: "",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
localConfig = mergeConfigs(localConfig, policyResponse);
@ -314,6 +316,7 @@ test("mergeConfigs does not override local allowed_endpoints if not empty", () =
one_time_key: "",
api_key: "",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: ["remote.endpoint:443"],
@ -345,6 +348,7 @@ test("mergeConfigs overrides disable_sudo_and_containers from remote", () => {
one_time_key: "",
api_key: "",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],
@ -375,6 +379,7 @@ test("mergeConfigs does not override fields when remote values are undefined", (
one_time_key: "",
api_key: "",
use_policy_store: false,
deploy_on_self_hosted_vm: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],

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 } from "./utils";
import { chownForFolder, detectThirdPartyRunnerProvider, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted } from "./utils";
import { buildBravoConfig } from "./bravo-config";
interface MonitorResponse {
runner_ip_address?: string;
@ -94,6 +96,7 @@ interface MonitorResponse {
one_time_key: "",
api_key: core.getInput("api-key"),
use_policy_store: core.getBooleanInput("use-policy-store"),
deploy_on_self_hosted_vm: core.getBooleanInput("deploy-on-self-hosted-vm"),
};
if (confg.api_key !== "") {
@ -293,13 +296,46 @@ 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",
});
core.info(common.SELF_HOSTED_RUNNER_MESSAGE);
if (confg.egress_policy === "block") {
const inContainer = isDocker();
const alreadyInstalled = isAgentInstalled(process.platform);
if (shouldDeployAgentOnSelfHosted(confg.deploy_on_self_hosted_vm, inContainer, alreadyInstalled)) {
if (process.platform !== "linux") {
core.info("deploy-on-self-hosted-vm is only supported on Linux. Skipping agent deployment.");
} else {
core.info("deploy-on-self-hosted-vm is enabled. Installing agent on self-hosted runner.");
await installAgentForSelfHosted(context.repo.owner, confg);
}
} else {
if (confg.deploy_on_self_hosted_vm && inContainer) {
core.info("Skipping agent deployment: running inside a container.");
}
if (confg.deploy_on_self_hosted_vm && alreadyInstalled) {
core.info("Agent already installed on self-hosted runner, skipping installation.");
}
}
if (confg.egress_policy === "block" && !confg.deploy_on_self_hosted_vm) {
sendAllowedEndpoints(confg.allowed_endpoints);
await sleep(5000);
}
@ -454,3 +490,112 @@ export function sleep(ms: number) {
setTimeout(resolve, ms);
});
}
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");
let isTLS = await isTLSEnabled(owner);
if (!isTLS) {
console.log("TLS is not enabled for this organization. Agent installation skipped for self-hosted runner.");
return;
}
const selfHostedConfig = {
customer: owner,
working_directory: confg.working_directory,
api_url: confg.api_url,
api_key: uuidv4(),
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,
is_github_hosted: false,
};
const selfHostedConfigStr = JSON.stringify(selfHostedConfig);
cp.execSync("sudo mkdir -p /home/agent");
chownForFolder(process.env.USER, "/home/agent");
const agentInstalled = await installAgent(isTLS, selfHostedConfigStr);
if (agentInstalled) {
const statusFile = "/home/agent/agent.status";
const logFile = "/home/agent/agent.log";
let counter = 0;
while (true) {
if (!fs.existsSync(statusFile)) {
counter++;
if (counter > 30) {
console.log("timed out");
if (fs.existsSync(logFile)) {
const content = fs.readFileSync(logFile, "utf-8");
console.log(content);
}
break;
}
await sleep(300);
} else {
const content = fs.readFileSync(statusFile, "utf-8");
console.log(content);
break;
}
}
}
} catch (error) {
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}`);
}
}

158
src/utils.test.ts Normal file
View file

@ -0,0 +1,158 @@
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs, detectThirdPartyRunnerProvider } from "./utils";
import * as fs from "fs";
jest.mock("fs", () => ({
...jest.requireActual("fs"),
existsSync: jest.fn(),
}));
const mockedExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
describe("shouldDeployAgentOnSelfHosted", () => {
test("returns true when deploy flag is true, not container, agent not installed", () => {
expect(shouldDeployAgentOnSelfHosted(true, false, false)).toBe(true);
});
test("returns false when deploy flag is false", () => {
expect(shouldDeployAgentOnSelfHosted(false, false, false)).toBe(false);
});
test("returns false when running in a container", () => {
expect(shouldDeployAgentOnSelfHosted(true, true, false)).toBe(false);
});
test("returns false when agent is already installed", () => {
expect(shouldDeployAgentOnSelfHosted(true, false, true)).toBe(false);
});
test("returns false when in container and agent installed", () => {
expect(shouldDeployAgentOnSelfHosted(true, true, true)).toBe(false);
});
test("returns false when all conditions are negative", () => {
expect(shouldDeployAgentOnSelfHosted(false, true, true)).toBe(false);
});
});
describe("isAgentInstalled", () => {
afterEach(() => {
mockedExistsSync.mockReset();
});
test("returns false for linux when status file does not exist", () => {
mockedExistsSync.mockReturnValue(false);
expect(isAgentInstalled("linux")).toBe(false);
expect(mockedExistsSync).toHaveBeenCalledWith("/home/agent/agent.status");
});
test("returns true for linux when status file exists", () => {
mockedExistsSync.mockReturnValue(true);
expect(isAgentInstalled("linux")).toBe(true);
});
test("returns false for win32 when status file does not exist", () => {
mockedExistsSync.mockReturnValue(false);
expect(isAgentInstalled("win32")).toBe(false);
expect(mockedExistsSync).toHaveBeenCalledWith("C:\\agent\\agent.status");
});
test("returns false for darwin when status file does not exist", () => {
mockedExistsSync.mockReturnValue(false);
expect(isAgentInstalled("darwin")).toBe(false);
expect(mockedExistsSync).toHaveBeenCalledWith("/opt/step-security/agent.status");
});
test("returns false for unsupported platform", () => {
expect(isAgentInstalled("freebsd" as NodeJS.Platform)).toBe(false);
});
});
describe("isPlatformSupported", () => {
test("returns true for linux", () => {
expect(isPlatformSupported("linux")).toBe(true);
});
test("returns true for win32", () => {
expect(isPlatformSupported("win32")).toBe(true);
});
test("returns true for darwin", () => {
expect(isPlatformSupported("darwin")).toBe(true);
});
test("returns false for unsupported platform", () => {
expect(isPlatformSupported("freebsd" as NodeJS.Platform)).toBe(false);
});
});
describe("getAnnotationLogs", () => {
test("throws for unsupported platform", () => {
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

@ -32,6 +32,25 @@ export function isAgentInstalled(platform: NodeJS.Platform) {
}
}
export function shouldDeployAgentOnSelfHosted(
deployOnSelfHostedVm: boolean,
isContainer: boolean,
agentAlreadyInstalled: boolean
): boolean {
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":