Compare commits

..
Author SHA1 Message Date
Varun Sharma bc791947a3 Merge pull request #634 from h0x0er/jatin/winmac
Add combined windows and macos changes
2026-02-07 22:10:19 -08:00
Jatin ec2fee99eb wait for done.json only if agent is installed 2026-02-03 17:59:09 +05:30
Jatin e823d39c9a Fixed verifyChecksum logic and updated macos checksum 2026-02-03 16:06:10 +05:30
Jatin 9a13b2d01e updated macos-installer download-url 2026-02-03 15:33:26 +05:30
Jatin 6061152f29 addressed comments 2026-02-03 12:04:06 +05:30
Jatin ac1419447b Read platform specific annotation-logs & added post-step check in
macos-cleanup
2026-02-02 17:40:08 +05:30
Jatin ef9c6372b3 combined windows and macos changes 2026-02-02 15:04:58 +05:30
24 changed files with 32521 additions and 25470 deletions
+7 -9
View File
@@ -19,20 +19,19 @@ Corporate laptops and production servers typically have robust security monitori
Traditional security monitoring and EDR solutions are ineffective for CI/CD runners due to their ephemeral nature. These tools also lack the necessary context to correlate events with specific workflow runs in a CI/CD environment.
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.
StepSecurity Harden-Runner addresses this gap by providing security monitoring tailored for CI/CD 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 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 CNCF’s 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)
- [Harden-Runner Detected a CI/CD Supply Chain Attack in Google’s Open-Source Project Flank](https://www.stepsecurity.io/case-studies/flank)
- [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)
- [Harden-Runner Detected a CI/CD Supply Chain Attack in Microsoft’s Open-Source Project Azure Karpenter Provider in Real-Time](https://www.stepsecurity.io/case-studies/azure-karpenter-provider)
- [Harden-Runner Detected Anomalous Traffic to api.ipify.org Across Multiple Customers](https://www.stepsecurity.io/blog/harden-runner-detects-anomalous-traffic-to-api-ipify-org-across-multiple-customers)
- [Harden-Runner Detected an Unexpected Microsoft Defender Installation on GitHub-Hosted Ubuntu Runners](https://www.stepsecurity.io/blog/how-stepsecurity-harden-runner-detected-unexpected-microsoft-defender-installation-on-github-hosted-ubuntu-runners)
- [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 **8 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 +69,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@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit
@@ -138,7 +137,7 @@ Explore the full feature set in the [Features Documentation](https://docs.stepse
## Trusted By and Case Studies
Harden-Runner is trusted by over 11,000 leading open-source projects and enterprises, including Microsoft, Google, Kubernetes, and more.
Harden-Runner is trusted by over 8000 leading open-source projects and enterprises, including Microsoft, Google, Kubernetes, and more.
### Trusted by
@@ -163,8 +162,7 @@ Harden-Runner is designed to work seamlessly across a variety of runner environm
| Environment Type | Compatibility | Audit Mode Deployment | Workflow Changes for Audit 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 |
| GitHub-hosted runners | ✅ Full support | Add Harden-Runner Action to workflow | Yes |
| Self-hosted VM runners | ✅ Full support | Include agent in runner image | No |
| Self-hosted bare-metal runners | ✅ Full support | Install agent as a service | No |
| Actions Runner Controller (ARC) | ✅ Full support | Deploy as DaemonSet | No |
@@ -178,7 +176,7 @@ Want to know the technical details? Dive into the architecture of Harden-Runner
## Limitations
While Harden-Runner offers powerful features, there are certain limitations. See the complete list in [Known Limitations](docs/limitations.md).
While Harden-Runner offers powerful features, there are certain limitations based on the environment, such as OS support. See the complete list in [Known Limitations](docs/limitations.md).
---
+2 -10
View File
@@ -29,23 +29,15 @@ inputs:
required: false
default: "false"
policy:
description: "Policy name to be used from the policy store. Requires id-token: write permission."
description: "Policy name to be used from the policy store"
required: false
default: ""
api-key:
description: "StepSecurity API key for authenticating with the policy store. Required when use-policy-store is set to true."
required: false
default: ""
use-policy-store:
description: "Set to true to fetch policy from the policy store using the API key. This is the preferred method over the policy input which requires id-token: write permission. Policies can be defined and attached at workflow, repo, org, or cluster (for ARC) level in the policy store. The most granular policy will apply."
required: false
default: "false"
branding:
icon: "check-square"
color: "green"
runs:
using: "node24"
using: "node20"
pre: "dist/pre/index.js"
main: "dist/index.js"
post: "dist/post/index.js"
+7449 -4948
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7447 -4952
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+17545 -15002
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -5,7 +5,8 @@
For GitHub-hosted runners, Harden-Runner GitHub Action downloads and installs the StepSecurity Agent.
- The code to monitor file, process, and network activity is in the Agent.
- The community tier agent for Linux is open-source and can be found [here](https://github.com/step-security/agent). The enterprise agent for Linux and agents for Windows and macOS are closed-source.
- The community tier agent is open-source and can be found [here](https://github.com/step-security/agent). The enterprise tier agent is closed-source. Both agents are written in Go.
- The agent's build is reproducible. You can view the steps to reproduce the build [here](http://app.stepsecurity.io/github/step-security/agent/releases/latest)
### Self-Hosted Actions Runner Controller (ARC) Runners
+6
View File
@@ -1,8 +1,14 @@
## Limitations
### GitHub-Hosted Runners
* Only Ubuntu VM is supported. Windows and MacOS GitHub-hosted runners are not supported. There is a discussion about that [here](https://github.com/step-security/harden-runner/discussions/121).
* Harden-Runner is not supported when [job is run in a container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) with built-in labels such as `ubuntu-latest`, as it needs sudo access on the Ubuntu VM to run. The limitation is if the entire job is run in a container. However, such jobs can be monitored when using custom VM images with GitHub-hosted runners. This is also not a limitation for Self-Hosted runners.
### Self-Hosted Actions Runner Controller (ARC) Runners
* Since ARC Harden Runner uses eBPF, only Linux jobs are supported. Windows and MacOS jobs are not supported.
### Self-Hosted VM (e.g. on EC2) and Bare-metal Runners
* Only Linux jobs are supported. Windows and MacOS jobs are not supported.
+1 -5
View File
@@ -8,8 +8,4 @@ reason = "Untrusted headers are not processed"
[[IgnoredVulns]]
id = "GHSA-xx4v-prfh-6cgc"
reason = "Untrusted headers are not processed"
[[IgnoredVulns]]
id = "GHSA-g9mf-h72j-4rw9"
reason = "undici fetch() is only used to call GitHub API; exploitation requires a malicious server"
reason = "Untrusted headers are not processed"
+39 -86
View File
@@ -2673,11 +2673,10 @@
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -3747,49 +3746,18 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"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": "5.0.9",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
"integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
"strnum": "^2.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -6210,10 +6178,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -6966,16 +6933,15 @@
}
},
"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.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
]
},
"node_modules/supports-color": {
"version": "7.2.0",
@@ -7018,16 +6984,15 @@
}
},
"node_modules/table/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
@@ -9566,9 +9531,9 @@
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
},
"ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -10364,24 +10329,12 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"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=="
},
"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.0.9",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
"integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==",
"requires": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
"strnum": "^2.0.5"
}
},
"fastq": {
@@ -12192,9 +12145,9 @@
"dev": true
},
"minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -12717,9 +12670,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.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q=="
},
"supports-color": {
"version": "7.2.0",
@@ -12750,15 +12703,15 @@
},
"dependencies": {
"ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"json-schema-traverse": {
+5 -5
View File
@@ -4,15 +4,15 @@ import * as fs from "fs";
const CHECKSUMS = {
tls: {
amd64: "d4b80f15758bb950787000e802cc58a565919a8cb9ecf405777b304ef42911fe", // v1.7.15
arm64: "3c224ea1da1776d1ba9f70b8dd8f0d8432230a7c2d464bca84bbdee8b7d46f6c",
amd64: "19c35eee1347077eb71306b122ad4a1cf83f36ef0f69fd91b0c0d79ffd0eabdd", // v1.7.10
arm64: "f9192788e86b2e44b795f072e8cc03eec9852649609aeedac0761d3b67c991fa",
},
non_tls: {
amd64: "4aaaeebbe10e619d8ce13e8cc4a1acbafc8f891e8cdd319984480b9ec08407b8", // v0.15.0
amd64: "336093af8ebe969567b66fd035af3bd4f7e1c723ce680d6b4b5b2a1f79bc329e", // v0.14.2
},
darwin: "797399a3a3f6f9c4c000a02e0d8c7b16499129c9bdc2ad9cf2a10072c10654fb", // v0.0.4
darwin: "eefb162810c378653c16e122e024314a2e47592dc98b295433b26ad1a4f28590", // v0.0.2
windows: {
amd64: "e98f8b9cf9ecf6566f1e16a470fbe4aef01610a644fd8203a1bab3ff142186c8", // v1.0.0
amd64: "9e4fde66331be3261ae6ff954e531e94335b5774ac7e105f0126b391ee1c6d66", // v1.0.0-int
},
};
-5
View File
@@ -204,11 +204,6 @@ async function handleWindowsCleanup() {
return;
}
if (process.arch === "arm64") {
console.log(common.ARM64_WINDOWS_RUNNER_MESSAGE);
return;
}
const p = cp.spawn(
"powershell.exe",
[
-3
View File
@@ -145,6 +145,3 @@ export const ARC_RUNNER_MESSAGE =
export const ARM64_RUNNER_MESSAGE =
"ARM runners are not supported in the Harden-Runner community tier.";
export const ARM64_WINDOWS_RUNNER_MESSAGE =
"Windows ARM runners are not yet supported by Harden-Runner.";
-3
View File
@@ -2,7 +2,4 @@ export const STEPSECURITY_ENV = "agent"; // agent or int
export const STEPSECURITY_API_URL = `https://${STEPSECURITY_ENV}.api.stepsecurity.io/v1`;
export const STEPSECURITY_TELEMETRY_URL =
"https://prod.app-api.stepsecurity.io/v1";
export const STEPSECURITY_WEB_URL = "https://app.stepsecurity.io";
+8 -9
View File
@@ -5,7 +5,7 @@ import * as path from "path";
import * as fs from "fs";
import { verifyChecksum } from "./checksum";
import { EOL } from "os";
import { ARM64_RUNNER_MESSAGE, ARM64_WINDOWS_RUNNER_MESSAGE } from "./common";
import { ARM64_RUNNER_MESSAGE } from "./common";
import { chownForFolder } from "./utils";
export async function installAgent(
@@ -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.7.10/harden-runner_1.7.10_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.14.2/agent_0.14.2_linux_amd64.tar.gz",
undefined,
auth
);
@@ -89,7 +89,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-int-releases/releases/download/v0.0.2-mac/macos-installer-0.0.2.tar.gz";
core.info(`Downloading macOS installer.. : ${downloadUrl}`);
const downloadPath = await tc.downloadTool(downloadUrl, undefined, auth);
core.info(`✓ Successfully downloaded installer to: ${downloadPath}`);
@@ -107,14 +107,13 @@ export async function installMacosAgent(configStr: string): Promise<boolean> {
// Copy Installer binary to /opt/step-security
const installerSourcePath = path.join(extractPath, "Installer");
const installerBinaryPath = "/opt/step-security/Installer";
core.info(
`Copying Installer from ${installerSourcePath} to /opt/step-security...`
);
cp.execFileSync("cp", [installerSourcePath, installerBinaryPath]);
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)) {
@@ -157,7 +156,7 @@ export async function installWindowsAgent(configStr: string): Promise<boolean> {
const variant = process.arch === "x64" ? "amd64" : "arm64";
if (variant === "arm64") {
console.log(ARM64_WINDOWS_RUNNER_MESSAGE);
console.log(ARM64_RUNNER_MESSAGE);
return false;
}
@@ -172,7 +171,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.0-int/harden-runner-agent-windows_int_windows_amd64.tar.gz`,
undefined,
auth
);
-4
View File
@@ -4,7 +4,6 @@ export interface Configuration {
correlation_id: string;
working_directory: string;
api_url: string;
telemetry_url: string;
allowed_endpoints: string;
egress_policy: string;
disable_telemetry: boolean;
@@ -15,8 +14,6 @@ export interface Configuration {
private: string;
is_debug: boolean;
one_time_key: string;
api_key: string;
use_policy_store: boolean;
}
export interface PolicyResponse {
@@ -28,5 +25,4 @@ export interface PolicyResponse {
disable_file_monitoring?: boolean;
disable_telemetry?: boolean;
egress_policy?: string;
policy_name?: string;
}
-312
View File
@@ -32,7 +32,6 @@ test("merge configs", async () => {
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "",
egress_policy: "audit",
disable_telemetry: false,
@@ -43,8 +42,6 @@ test("merge configs", async () => {
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
owner: "h0x0er",
@@ -62,7 +59,6 @@ test("merge configs", async () => {
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "github.com:443 google.com:443",
egress_policy: "audit",
disable_telemetry: false,
@@ -73,316 +69,8 @@ test("merge configs", async () => {
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig).toStrictEqual(expectedConfiguration);
});
// ==================== additional fetchPolicy tests ====================
test("fetchPolicy throws when idToken is empty", async () => {
await expect(fetchPolicy("owner", "policy1", "")).rejects.toThrow(
"[PolicyFetch]: id-token in empty"
);
});
test("fetchPolicy retries on failure and succeeds", async () => {
const owner = "test-owner";
const policyName = "test-policy";
const response = {
allowed_endpoints: ["example.com:443"],
egress_policy: "block",
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.replyWithError("connection timeout");
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.reply(200, response);
const policy = await fetchPolicy(owner, policyName, "token123");
expect(policy).toStrictEqual(response);
});
test("fetchPolicy throws after all retries exhausted", async () => {
const owner = "test-owner";
const policyName = "test-policy";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.times(3)
.replyWithError("connection timeout");
await expect(
fetchPolicy(owner, policyName, "token123")
).rejects.toThrow("[Policy Fetch]");
});
test("fetchPolicy preserves statusCode from error", async () => {
const owner = "test-owner";
const policyName = "test-policy";
const errorWithStatus = new Error("Not Found");
(errorWithStatus as any).statusCode = 404;
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/actions/policies/${policyName}`)
.times(3)
.replyWithError(errorWithStatus);
try {
await fetchPolicy(owner, policyName, "token123");
fail("should have thrown");
} catch (err) {
expect(err.message).toContain("[Policy Fetch]");
}
});
// ==================== fetchPolicyFromStore ====================
import { fetchPolicyFromStore } from "./policy-utils";
const policyStoreQueryString = (workflow: string, runId: string, correlationId: string) =>
`workflow=${encodeURIComponent(workflow)}&run_id=${encodeURIComponent(runId)}&correlationId=${encodeURIComponent(correlationId)}`;
test("success: fetches policy from store", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const response = {
allowed_endpoints: ["registry.npmjs.org:443", "github.com:443"],
egress_policy: "block",
disable_sudo: true,
disable_file_monitoring: false,
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, response);
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toStrictEqual(response);
});
test("fetchPolicyFromStore throws when apiKey is empty", async () => {
await expect(
fetchPolicyFromStore("owner", "repo", "", "ci.yml", "123", "abc")
).rejects.toThrow("[PolicyStoreFetch]: api-key is empty");
});
test("fetchPolicyFromStore returns null when policy not found (404)", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(404, { message: "not found" });
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toBeNull();
});
test("fetchPolicyFromStore returns null when API returns empty policy", async () => {
const owner = "test-owner";
const repo = "nonexistent-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, { allowed_endpoints: [], egress_policy: "", policy_name: "" });
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toBeNull();
});
test("fetchPolicyFromStore retries on failure and succeeds", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const response = {
allowed_endpoints: ["example.com:443"],
egress_policy: "audit",
};
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.replyWithError("timeout");
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, response);
const result = await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
expect(result).toStrictEqual(response);
});
test("fetchPolicyFromStore throws after all retries exhausted", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.times(3)
.replyWithError("connection refused");
await expect(
fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId)
).rejects.toThrow("[Policy Store Fetch]");
});
test("fetchPolicyFromStore preserves statusCode from error", async () => {
const owner = "test-owner";
const repo = "test-repo";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
const errorWithStatus = new Error("Unauthorized");
(errorWithStatus as any).statusCode = 401;
nock(`${STEPSECURITY_API_URL}`)
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.times(3)
.replyWithError(errorWithStatus);
try {
await fetchPolicyFromStore(owner, repo, "my-api-key", workflow, runId, correlationId);
fail("should have thrown");
} catch (err) {
expect(err.message).toContain("[Policy Store Fetch]");
}
});
test("fetchPolicyFromStore sends correct authorization header", async () => {
const owner = "test-owner";
const repo = "test-repo";
const apiKey = "secret-key-123";
const workflow = "ci.yml";
const runId = "12345";
const correlationId = "abc-def";
nock(`${STEPSECURITY_API_URL}`, {
reqheaders: {
Authorization: `vm-api-key ${apiKey}`,
Source: "github-actions",
},
})
.get(`/github/${owner}/${repo}/actions/policies/workflow-policy?${policyStoreQueryString(workflow, runId, correlationId)}`)
.reply(200, { allowed_endpoints: [], egress_policy: "audit" });
const result = await fetchPolicyFromStore(owner, repo, apiKey, workflow, runId, correlationId);
expect(result).toStrictEqual({
allowed_endpoints: [],
egress_policy: "audit",
});
});
// ==================== additional mergeConfigs tests ====================
test("mergeConfigs does not override local allowed_endpoints if not empty", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "local.endpoint:443",
egress_policy: "audit",
disable_telemetry: false,
disable_sudo: false,
disable_sudo_and_containers: false,
disable_file_monitoring: false,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: ["remote.endpoint:443"],
egress_policy: "block",
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.allowed_endpoints).toBe("local.endpoint:443");
expect(localConfig.egress_policy).toBe("block");
});
test("mergeConfigs overrides disable_sudo_and_containers from remote", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "",
egress_policy: "audit",
disable_telemetry: false,
disable_sudo: false,
disable_sudo_and_containers: false,
disable_file_monitoring: false,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],
disable_sudo_and_containers: true,
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.disable_sudo_and_containers).toBe(true);
});
test("mergeConfigs does not override fields when remote values are undefined", () => {
let localConfig: Configuration = {
repo: "test/repo",
run_id: "xyx",
correlation_id: "aaaaa",
working_directory: "/xyz",
api_url: "xyz",
telemetry_url: "xyz",
allowed_endpoints: "",
egress_policy: "block",
disable_telemetry: false,
disable_sudo: true,
disable_sudo_and_containers: true,
disable_file_monitoring: true,
private: "true",
is_github_hosted: true,
is_debug: false,
one_time_key: "",
api_key: "",
use_policy_store: false,
};
let policyResponse: PolicyResponse = {
allowed_endpoints: [],
};
localConfig = mergeConfigs(localConfig, policyResponse);
expect(localConfig.disable_sudo).toBe(true);
expect(localConfig.disable_sudo_and_containers).toBe(true);
expect(localConfig.disable_file_monitoring).toBe(true);
expect(localConfig.egress_policy).toBe("block");
});
-59
View File
@@ -50,65 +50,6 @@ export async function fetchPolicy(
}
}
export async function fetchPolicyFromStore(
owner: string,
repo: string,
apiKey: string,
workflow: string,
runId: string,
correlationId: string
): Promise<PolicyResponse | null> {
if (apiKey === "") {
throw new Error("[PolicyStoreFetch]: api-key is empty");
}
let policyEndpoint = `${STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/policies/workflow-policy?workflow=${encodeURIComponent(workflow)}&run_id=${encodeURIComponent(runId)}&correlationId=${encodeURIComponent(correlationId)}`;
let httpClient = new HttpClient();
let headers = {};
headers["Authorization"] = `vm-api-key ${apiKey}`;
headers["Source"] = "github-actions";
let response = undefined;
let err = undefined;
let retry = 0;
while (retry < 3) {
try {
console.log(`Attempt: ${retry + 1}`);
response = await httpClient.getJson<PolicyResponse>(
policyEndpoint,
headers
);
break;
} catch (e) {
err = e;
}
retry += 1;
await sleep(1000);
}
if (response === undefined && err !== undefined) {
const error = new Error(`[Policy Store Fetch] ${err}`);
if (err.statusCode !== undefined) {
(error as any).statusCode = err.statusCode;
}
throw error;
}
if (response.statusCode === 404) {
return null;
}
const result = response.result;
if (!result || (!result.egress_policy && (!result.allowed_endpoints || result.allowed_endpoints.length === 0))) {
return null;
}
return result;
}
export function mergeConfigs(
localConfig: Configuration,
remoteConfig: PolicyResponse
+4 -51
View File
@@ -16,7 +16,7 @@ import {
isValidEvent,
} from "./cache";
import { Configuration, PolicyResponse } from "./interfaces";
import { fetchPolicy, fetchPolicyFromStore, mergeConfigs } from "./policy-utils";
import { fetchPolicy, mergeConfigs } from "./policy-utils";
import * as cache from "@actions/cache";
import { getCacheEntry } from "@actions/cache/lib/internal/cacheHttpClient";
import * as cacheTwirpClient from "@actions/cache/lib/internal/shared/cacheTwirpClient";
@@ -25,11 +25,7 @@ import { getCacheServiceVersion } from "@actions/cache/lib/internal/config";
import * as utils from "@actions/cache/lib/internal/cacheUtils";
import { isARCRunner, sendAllowedEndpoints } from "./arc-runner";
import {
STEPSECURITY_API_URL,
STEPSECURITY_TELEMETRY_URL,
STEPSECURITY_WEB_URL,
} from "./configs";
import { STEPSECURITY_API_URL, STEPSECURITY_WEB_URL } from "./configs";
import { isGithubHosted, isTLSEnabled } from "./tls-inspect";
import {
installAgent,
@@ -74,7 +70,6 @@ interface MonitorResponse {
correlation_id: correlation_id,
working_directory: process.env["GITHUB_WORKSPACE"],
api_url: api_url,
telemetry_url: STEPSECURITY_TELEMETRY_URL,
allowed_endpoints: core.getInput("allowed-endpoints"),
egress_policy: core.getInput("egress-policy"),
disable_telemetry: core.getBooleanInput("disable-telemetry"),
@@ -87,51 +82,10 @@ interface MonitorResponse {
is_github_hosted: isGithubHosted(),
is_debug: core.isDebug(),
one_time_key: "",
api_key: core.getInput("api-key"),
use_policy_store: core.getBooleanInput("use-policy-store"),
};
if (confg.api_key !== "") {
core.setSecret(confg.api_key);
}
let policyName = core.getInput("policy");
if (confg.use_policy_store) {
console.log(`Fetching policy from policy store`);
if (confg.api_key === "") {
core.setFailed("api-key is required when use-policy-store is set to true");
} else {
try {
const repoName = (process.env["GITHUB_REPOSITORY"] || "").split("/")[1] || "";
const workflowRef = process.env["GITHUB_WORKFLOW_REF"] || "";
const workflow = workflowRef.replace(/.*\.github\/workflows\//, "").replace(/@.*/, "");
let result: PolicyResponse | null = await fetchPolicyFromStore(
context.repo.owner,
repoName,
confg.api_key,
workflow,
confg.run_id,
confg.correlation_id
);
if (result !== null) {
core.info(`Policy found: ${result.policy_name || "unnamed"}`);
confg = mergeConfigs(confg, result);
} else {
core.info("No policy found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
}
} catch (err) {
core.info(`[!] ${err}`);
if (err.statusCode >= 400 && err.statusCode < 500) {
core.info("Policy not found in policy store. Defaulting to audit mode.");
confg.egress_policy = "audit";
} else {
core.error(`Unexpected error fetching from policy store: ${err}. Falling back to audit mode.`);
confg.egress_policy = "audit";
}
}
}
} else if (policyName !== "") {
if (policyName !== "") {
console.log(`Fetching policy from API with name: ${policyName}`);
try {
let idToken: string = await core.getIDToken();
@@ -373,8 +327,7 @@ interface MonitorResponse {
return;
}
const { api_key, use_policy_store, ...agentConfig } = confg;
const configStr = JSON.stringify(agentConfig);
const configStr = JSON.stringify(confg);
// platform specific
let statusFile = "";