mirror of
https://github.com/step-security/harden-runner
synced 2026-08-09 13:11:02 +00:00
Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6891e502d | ||
|
|
201a0acb8b | ||
|
|
6a90cbd27b | ||
|
|
742dfd684f | ||
|
|
ac63a71186 | ||
|
|
a90bcbc653 | ||
|
|
f0a59d8853 | ||
|
|
5ef0c079ce | ||
|
|
eb43c7b3fd | ||
|
|
e3f713f2d8 | ||
|
|
423acdda6f | ||
|
|
0ddb86cf03 | ||
|
|
20cf305ff2 | ||
|
|
c51e8eeb6c | ||
|
|
e152b90204 | ||
|
|
ee1faec052 | ||
|
|
1dc7c17646 | ||
|
|
df199fb7be | ||
|
|
03d096a772 | ||
|
|
40901073af | ||
|
|
95d9a5deda | ||
|
|
87e429d3fb | ||
|
|
ef891c3a30 | ||
|
|
1fa8c8a8b1 | ||
|
|
92c522aaa6 | ||
|
|
4719ad5578 | ||
|
|
4fde639ab4 | ||
|
|
f682f2f2d0 | ||
|
|
f4a75cfd61 | ||
|
|
95503d076c | ||
|
|
4b250a0739 | ||
|
|
5b0ab6abcf | ||
|
|
d11f2c1d65 | ||
|
|
b3fc98e4df | ||
|
|
92fc5d4bf7 | ||
|
|
b61b0a4938 | ||
|
|
e3d3f2baea | ||
|
|
646ac01e72 | ||
|
|
7bc18df383 | ||
|
|
17d38b322b | ||
|
|
ec9f2d5744 | ||
|
|
04bcbc31cf | ||
|
|
7c7a56fcaa | ||
|
|
6c439dc8bd | ||
|
|
bf5688696d | ||
|
|
5436dac7b5 | ||
|
|
88d305a353 | ||
|
|
b976878278 | ||
|
|
875cc92db2 | ||
|
|
002fdce3c6 | ||
|
|
2489e3fcb3 | ||
|
|
75dd441a81 | ||
|
|
4381ace9c4 | ||
|
|
a9da90b635 | ||
|
|
a60ef21c0c | ||
|
|
4ad512f165 | ||
|
|
6b41a39235 | ||
|
|
fa70c45ca9 | ||
|
|
eb47845632 | ||
|
|
1705d777e5 | ||
|
|
62893838e7 | ||
|
|
42a3378d67 | ||
|
|
f0cb479b0f | ||
|
|
9abfa72990 | ||
|
|
4379a81ea6 | ||
|
|
5a5cdce402 | ||
|
|
1106c3d7dd | ||
|
|
0634a2670c | ||
|
|
2e3c511341 | ||
|
|
40873e6a41 | ||
|
|
484c2799ec | ||
|
|
4c8582f455 | ||
|
|
e8d595cd66 | ||
|
|
5d277fc873 | ||
|
|
ff2ab228bd | ||
|
|
b81d650d0e | ||
|
|
769df4ef5d | ||
|
|
a7a8a29fbc | ||
|
|
75fd6ee0fe | ||
|
|
86338660c4 | ||
|
|
230ee49196 | ||
|
|
617de1d11d | ||
|
|
d930a8a3b2 | ||
|
|
c6295a65d1 | ||
|
|
3e118b145b | ||
|
|
b38e918ba8 | ||
|
|
0664d30cda | ||
|
|
b131ca5ebf | ||
|
|
2dc9579753 | ||
|
|
f054d811b5 | ||
|
|
8a09271fed | ||
|
|
6ec6af7d62 | ||
|
|
539365ba33 | ||
|
|
e34e1ce869 | ||
|
|
4cfb9f26cc | ||
|
|
8b7e2a634c | ||
|
|
18fe9c66a7 | ||
|
|
bb31c5e3c0 | ||
|
|
bf5f99ab75 | ||
|
|
db60402296 | ||
|
|
41d3351872 | ||
|
|
27a99dbdfc | ||
|
|
0f3c98aa98 | ||
|
|
e8e3270940 |
@@ -1,23 +0,0 @@
|
||||
name: Code Review
|
||||
on:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
code-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
int.api.stepsecurity.io:443
|
||||
|
||||
- name: Code Review
|
||||
uses: step-security/ai-codewise@int
|
||||
@@ -3,12 +3,15 @@ name: RunsOn Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-host-outbound:
|
||||
runs-on:
|
||||
- runs-on=${{ github.run_id }}
|
||||
- runner=2cpu-linux-x64
|
||||
- image=ubuntu22-stepsecurity-x64
|
||||
- image=ubuntu24-stepsecurity-x64
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@rc
|
||||
@@ -37,7 +40,7 @@ jobs:
|
||||
runs-on:
|
||||
- runs-on=${{ github.run_id }}
|
||||
- runner=2cpu-linux-x64
|
||||
- image=ubuntu22-stepsecurity-x64
|
||||
- image=ubuntu24-stepsecurity-x64
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@rc
|
||||
@@ -83,7 +86,7 @@ jobs:
|
||||
runs-on:
|
||||
- runs-on=${{ github.run_id }}
|
||||
- runner=2cpu-linux-x64
|
||||
- image=ubuntu22-stepsecurity-x64
|
||||
- image=ubuntu24-stepsecurity-x64
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@rc
|
||||
@@ -131,7 +134,7 @@ jobs:
|
||||
runs-on:
|
||||
- runs-on=${{ github.run_id }}
|
||||
- runner=2cpu-linux-x64
|
||||
- image=ubuntu22-stepsecurity-x64
|
||||
- image=ubuntu24-stepsecurity-x64
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@rc
|
||||
@@ -173,4 +176,4 @@ jobs:
|
||||
# Stop the container
|
||||
docker stop long-running-container
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # tag=v3.0.0
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # tag=v4.6.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
|
||||
@@ -39,8 +39,37 @@ jobs:
|
||||
run: npm test -- --coverage
|
||||
- uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
|
||||
- name: Publish Test Results
|
||||
uses: step-security/publish-unit-test-result-action@v2
|
||||
uses: step-security/publish-unit-test-result-action@e88bfc6c0dffc68a1067d63526c80c81f248da11 # v2.20.0
|
||||
if: always()
|
||||
with:
|
||||
files: |
|
||||
reports/*.xml
|
||||
|
||||
build-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
registry.npmjs.org:443
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
- name: Check for changes in dist
|
||||
run: |
|
||||
if [[ `git status --porcelain dist` ]]; then
|
||||
echo "Changes detected in dist directory after build:"
|
||||
git status --porcelain dist
|
||||
git diff dist
|
||||
exit 1
|
||||
else
|
||||
echo "No changes in dist directory - build is clean"
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="images/banner.png" width="400">
|
||||
<img alt="Dark Banner" src="images/banner-dark.png" width="400">
|
||||
<source media="(prefers-color-scheme: light)" srcset="images/harden-runner-new.png" width="400">
|
||||
<img alt="Dark Banner" src="images/harden-runner-new.png" width="400">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -15,31 +15,51 @@
|
||||
|
||||
# Harden-Runner
|
||||
|
||||
Harden-Runner secures CI/CD workflows by controlling network access and monitoring activities on GitHub-hosted and self-hosted runners. It blocks unauthorized network traffic and detects unusual activity to protect against potential threats. The name "Harden-Runner" comes from its purpose: strengthening the security of the runners used in GitHub Actions workflows.
|
||||
Corporate laptops and production servers typically have robust security monitoring in place to reduce risk and meet compliance requirements. However, CI/CD runners, which handle sensitive information like secrets for cloud environments and create production builds, often lack such security measures. This oversight has led to significant supply chain attacks, including the SolarWinds and Codecov breaches.
|
||||
|
||||
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.
|
||||
### 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 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:
|
||||
➡️ [Who's using Harden-Runner?](https://docs.stepsecurity.io/whos-using-harden-runner)
|
||||
|
||||
## Quick Links
|
||||
- [Getting Started Guide](#getting-started)
|
||||
- [Why Choose Harden-Runner](#why-choose-harden-runner)
|
||||
- [Features and Capabilities](#features)
|
||||
- [Case Studies and Trusted Projects](#trusted-by-and-case-studies)
|
||||
- [Environment Compatibility Matrix](#environment-compatibility-matrix)
|
||||
- [How It Works](docs/how-it-works.md)
|
||||
- [Known Limitations](docs/limitations.md)
|
||||
- [Join the Discussions](#discussions)
|
||||
- [Official Documentation](https://docs.stepsecurity.io/harden-runner)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Learn how Harden-Runner works through the video below, which shows how it detected a supply chain attack on a **Google** open-source project.
|
||||
Learn how Harden-Runner works through the video below, which shows how it detected the tj-actions/changed-files compromise. [View the interactive demo here](https://app.storylane.io/share/679y2zgzljov).
|
||||
|
||||
<a href="https://youtu.be/Yz72qAOrN9s" target="_blank">
|
||||
<img src="images/case-study-thumbnail1.png" alt="Harden-Runner detected supply chain attack in a Google open-source project" title="This case study video shows how StepSecurity Harden-Runner detected a CI/CD supply chain attack in real-time in Google’s open-source project Flank">
|
||||
<a href="https://www.youtube.com/watch?v=oTAJ7Hy7nPU" target="_blank">
|
||||
<img src="images/case-study.png" alt="Harden-Runner detected the tj-actions/changed-files Breach" title="This case study video shows how StepSecurity Harden-Runner detected the tj-actions/changed-files Breach">
|
||||
</a>
|
||||
|
||||
---
|
||||
## Getting Started
|
||||
|
||||
This guide walks you through the steps to set up and use Harden-Runner in your CI/CD workflows.
|
||||
This guide walks you through the steps to set up and use Harden-Runner in your CI/CD workflows. For more details, refer to the [official documentation](https://docs.stepsecurity.io/harden-runner).
|
||||
|
||||
### **Step 1: Add Harden-Runner to Your Workflow**
|
||||
|
||||
@@ -49,11 +69,12 @@ To integrate Harden-Runner, follow these steps:
|
||||
- Add the following code as the first step in each job:
|
||||
```yaml
|
||||
steps:
|
||||
- uses: step-security/harden-runner@446798f8213ac2e75931c1b0769676d927801858 # v2.10.3
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
**Tip**: Automate this step by pasting your workflow into the [StepSecurity online tool](https://app.stepsecurity.io/secureworkflow)
|
||||
**Tip**: Automate this step by pasting your workflow into the [StepSecurity online tool](https://app.stepsecurity.io/secure-workflow)
|
||||
<details>
|
||||
<summary>Click to view the full Getting Started Guide</summary>
|
||||
|
||||
@@ -65,7 +86,7 @@ Run your workflow. Once completed:
|
||||
<p align="left">
|
||||
<img src="images/buildlog1.png" alt="Link in workflow log" >
|
||||
</p>
|
||||
- Click on the provided link (e.g., [example link](https://appv2.stepsecurity.io/github/step-security/github-actions-goat/actions/runs/7704454287?jobid=20996777560&tab=network-events)) to access the **Process Monitor View**, which displays:
|
||||
- Click on the provided link (e.g., [example link](https://app.stepsecurity.io/github/step-security/github-actions-goat/actions/runs/7704454287?jobid=20996777560&tab=network-events)) to access the **Process Monitor View**, which displays:
|
||||
- **Network events**: Outbound network calls correlated with each step.
|
||||
- **File events**: File writes tracked during the job.
|
||||
<p align="left">
|
||||
@@ -91,11 +112,11 @@ Harden-Runner offers a comprehensive suite of features to enhance the security o
|
||||
|
||||
### Community (Free)
|
||||
|
||||
- **Block Network Egress Traffic with Domain Allowlist:** Control outbound network traffic by specifying allowed domains, preventing unauthorized data exfiltration.
|
||||
- **Detect Compromised Packages, Dependencies & Build Tools:** Identify and mitigate risks from malicious or vulnerable components in your build process.
|
||||
- **CI/CD-Aware Event Correlation:** Each outbound network connection, file operation, and process execution is mapped to the exact step, job, and workflow where it occurs.
|
||||
- **Automated Baseline Creation:** Harden-Runner builds a baseline for each job based on past outbound network connections.
|
||||
- **Anomaly Detection:** Once the baseline is created, any future outbound calls not in the baseline trigger a detection.
|
||||
- **Block Network Egress Traffic with Domain Allowlist:** Optionally use the automatically created baseline to control outbound network traffic by specifying allowed domains, preventing unauthorized data exfiltration.
|
||||
- **Detect Modification of Source Code:** Monitor and alert on unauthorized changes to your source code during the CI/CD pipeline.
|
||||
- **Disable Sudo Access:** Restrict the use of superuser privileges in your workflows to minimize security risks.
|
||||
- **Insights Page for CI/CD Runs:** Access detailed reports and analytics for each CI/CD run to monitor security events and compliance.
|
||||
|
||||
### Enterprise (Paid)
|
||||
|
||||
@@ -103,11 +124,11 @@ Includes all features in the **Community** tier, plus:
|
||||
|
||||
- **Support for Private Repositories:** Extend Harden-Runner's security capabilities to your private GitHub repositories.
|
||||
- **Support for Self-Hosted Runners:** Apply security controls and monitoring to self-hosted GitHub Actions runners.
|
||||
- **GitHub Checks Integration:** Enable GitHub Checks for Harden-Runner—if the baseline remains unchanged, the check passes; if it changes, the check fails, showing new outbound connections.
|
||||
- **View Outbound GitHub API calls at the Job Level:** Monitor HTTPS requests to GitHub APIs
|
||||
- **Determine Minimum GITHUB_TOKEN Permissions:** Monitor outbound HTTPS requests to GitHub APIs to recommend the least-privilege permissions needed for your workflows, enhancing security by reducing unnecessary access.
|
||||
- **View the Name and Path of Every File Written During the Build Process:** Gain visibility into every file written to the build environment, including the ability to correlate file writes with processes, ensuring complete transparency.
|
||||
- **View Process Names and Arguments:** Monitor every process executed during the build process, along with its arguments, and navigate the process tree to detect suspicious activities.
|
||||
- **Github Checks:** Display Harden Runner insights in the GitHub Checks UI, giving quick feedback on unusual network activity in pull requests.
|
||||
|
||||
For a detailed comparison and more information, please visit our [Pricing Page](https://www.stepsecurity.io/pricing).
|
||||
|
||||
@@ -117,26 +138,38 @@ Explore the full feature set in the [Features Documentation](https://docs.stepse
|
||||
|
||||
## Trusted By and Case Studies
|
||||
|
||||
Harden-Runner is trusted by over 5000 leading open-source projects and enterprises, including Microsoft, Google, Kubernetes, and more.
|
||||
Harden-Runner is trusted by over 11,000 leading open-source projects and enterprises, including Microsoft, Google, Kubernetes, and more.
|
||||
|
||||
### Trusted by
|
||||
|
||||
|
||||
| [](https://appv2.stepsecurity.io/github/cisagov/skeleton-generic/actions/runs/9947319332?jobid=27479776091&tab=network-events) | [](https://appv2.stepsecurity.io/github/microsoft/ebpf-for-windows/actions/runs/7587031851) | [](https://appv2.stepsecurity.io/github/GoogleCloudPlatform/functions-framework-ruby/actions/runs/7576989995) | [](https://appv2.stepsecurity.io/github/DataDog/stratus-red-team/actions/runs/7446169664) | [](https://appv2.stepsecurity.io/github/intel/cve-bin-tool/actions/runs/7590975903) | [](https://appv2.stepsecurity.io/github/kubernetes-sigs/cluster-api-provider-azure/actions/runs/7591172950) | [](https://appv2.stepsecurity.io/github/nodejs/node/actions/runs/7591405720) | [](https://appv2.stepsecurity.io/github/aws/aperf/actions/runs/7631366761) |
|
||||
| [](https://app.stepsecurity.io/github/cisagov/skeleton-generic/actions/runs/9947319332?jobid=27479776091&tab=network-events) | [](https://app.stepsecurity.io/github/microsoft/ebpf-for-windows/actions/runs/7587031851) | [](https://app.stepsecurity.io/github/GoogleCloudPlatform/functions-framework-ruby/actions/runs/7576989995) | [](https://app.stepsecurity.io/github/DataDog/stratus-red-team/actions/runs/7446169664) | [](https://app.stepsecurity.io/github/intel/cve-bin-tool/actions/runs/7590975903) | [](https://app.stepsecurity.io/github/kubernetes-sigs/cluster-api-provider-azure/actions/runs/7591172950) | [](https://app.stepsecurity.io/github/nodejs/node/actions/runs/7591405720) | [](https://app.stepsecurity.io/github/aws/aperf/actions/runs/7631366761) |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **CISA**<br>[Explore](https://appv2.stepsecurity.io/github/cisagov/skeleton-generic/actions/runs/9947319332?jobid=27479776091&tab=network-events) | **Microsoft**<br>[Explore](https://appv2.stepsecurity.io/github/microsoft/ebpf-for-windows/actions/runs/7587031851) | **Google**<br>[Explore](https://appv2.stepsecurity.io/github/GoogleCloudPlatform/functions-framework-ruby/actions/runs/7576989995) | **DataDog**<br>[Explore](https://appv2.stepsecurity.io/github/DataDog/stratus-red-team/actions/runs/7446169664) | **Intel**<br>[Explore](https://appv2.stepsecurity.io/github/intel/cve-bin-tool/actions/runs/7590975903) | **Kubernetes**<br>[Explore](https://appv2.stepsecurity.io/github/kubernetes-sigs/cluster-api-provider-azure/actions/runs/7591172950) | **Node.js**<br>[Explore](https://appv2.stepsecurity.io/github/nodejs/node/actions/runs/7591405720) | **AWS**<br>[Explore](https://appv2.stepsecurity.io/github/aws/aperf/actions/runs/7631366761) |
|
||||
| **CISA**<br>[Explore](https://app.stepsecurity.io/github/cisagov/skeleton-generic/actions/runs/9947319332?jobid=27479776091&tab=network-events) | **Microsoft**<br>[Explore](https://app.stepsecurity.io/github/microsoft/ebpf-for-windows/actions/runs/7587031851) | **Google**<br>[Explore](https://app.stepsecurity.io/github/GoogleCloudPlatform/functions-framework-ruby/actions/runs/7576989995) | **DataDog**<br>[Explore](https://app.stepsecurity.io/github/DataDog/stratus-red-team/actions/runs/7446169664) | **Intel**<br>[Explore](https://app.stepsecurity.io/github/intel/cve-bin-tool/actions/runs/7590975903) | **Kubernetes**<br>[Explore](https://app.stepsecurity.io/github/kubernetes-sigs/cluster-api-provider-azure/actions/runs/7591172950) | **Node.js**<br>[Explore](https://app.stepsecurity.io/github/nodejs/node/actions/runs/7591405720) | **AWS**<br>[Explore](https://app.stepsecurity.io/github/aws/aperf/actions/runs/7631366761) |
|
||||
|
||||
### Case Studies
|
||||
### Enterprise Case Studies
|
||||
|
||||
- [Harden-Runner Detects CI/CD Supply Chain Attack in Google’s Open-Source Project Flank](https://www.stepsecurity.io/case-studies/flank)
|
||||
- [StepSecurity Detects 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)
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
||||
## Environment Compatibility Matrix
|
||||
|
||||
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 |
|
||||
|------------------|---------------|--------------------------|-------------------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| RunsOn Runners | ✅ Full support | Pre-integrated | No |
|
||||
|
||||
## How It Works
|
||||
|
||||
Want to know the technical details? Dive into the architecture of Harden-Runner and its integrations for GitHub-hosted and self-hosted runners in our [How Harden-Runner Works Documentation](docs/how-it-works.md).
|
||||
@@ -145,7 +178,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 based on the environment, such as OS support. See the complete list in [Known Limitations](docs/limitations.md).
|
||||
While Harden-Runner offers powerful features, there are certain limitations. See the complete list in [Known Limitations](docs/limitations.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+5
-1
@@ -17,7 +17,11 @@ inputs:
|
||||
required: false
|
||||
default: "false"
|
||||
disable-sudo:
|
||||
description: "Disable sudo access for the runner account"
|
||||
description: "Disable sudo access for the runner account. Note: This parameter will be deprecated in the future. Please use disable-sudo-and-containers instead."
|
||||
required: false
|
||||
default: "false"
|
||||
disable-sudo-and-containers:
|
||||
description: "Disable sudo and container access for the runner account"
|
||||
required: false
|
||||
default: "false"
|
||||
disable-file-monitoring:
|
||||
|
||||
Vendored
+29851
-786
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+30080
-838
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+63106
-49104
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,8 +5,7 @@
|
||||
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 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)
|
||||
- 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.
|
||||
|
||||
### Self-Hosted Actions Runner Controller (ARC) Runners
|
||||
|
||||
|
||||
+1
-7
@@ -1,14 +1,8 @@
|
||||
## 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) as it needs sudo access on the Ubuntu VM to run. It can be used to monitor jobs that use containers to run steps. The limitation is if the entire job is run in a container. That is not common for GitHub Actions workflows, as most of them run directly on `ubuntu-latest`. Note: This is not a limitation for Self-Hosted runners.
|
||||
* 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 Runners (e.g. on EC2)
|
||||
|
||||
* Only Ubuntu VM is supported. Windows and MacOS jobs are not supported.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 407 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1,15 @@
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-h5c3-5r3r-rr8q"
|
||||
reason = "Untrusted headers are not processed"
|
||||
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-rmvr-2pp2-xj38"
|
||||
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"
|
||||
Generated
+1378
-974
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -23,12 +23,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/step-security/harden-runner#readme",
|
||||
"dependencies": {
|
||||
"@actions/cache": "^3.1.4",
|
||||
"@actions/cache": "^4.0.0",
|
||||
"@actions/core": "^1.5.0",
|
||||
"@actions/exec": "^1.1.0",
|
||||
"@actions/github": "^5.0.0",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@actions/tool-cache": "^1.7.1",
|
||||
"@babel/helpers": "^7.26.10",
|
||||
"ansi-regex": ">=5.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
"node-fetch": ">=3.2.0",
|
||||
@@ -39,7 +40,7 @@
|
||||
"@types/node": "^16.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"@vercel/ncc": "^0.30.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"jest": "^29.3.1",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isArcRunner, sendAllowedEndpoints } from "./arc-runner";
|
||||
import { isARCRunner } from "./arc-runner";
|
||||
|
||||
it("should correctly recognize arc based runner", async () => {
|
||||
process.env["GITHUB_ACTIONS_RUNNER_EXTRA_USER_AGENT"] =
|
||||
"actions-runner-controller/2.0.1";
|
||||
|
||||
let isArc: boolean = await isArcRunner();
|
||||
let isArc: boolean = await isARCRunner();
|
||||
expect(isArc).toBe(true);
|
||||
});
|
||||
|
||||
+16
-6
@@ -2,7 +2,7 @@ import * as cp from "child_process";
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function isArcRunner(): boolean {
|
||||
export function isARCRunner(): boolean {
|
||||
const runnerUserAgent = process.env["GITHUB_ACTIONS_RUNNER_EXTRA_USER_AGENT"];
|
||||
|
||||
let isARC = false;
|
||||
@@ -18,23 +18,33 @@ export function isArcRunner(): boolean {
|
||||
|
||||
function isSecondaryPod(): boolean {
|
||||
const workDir = "/__w";
|
||||
return fs.existsSync(workDir);
|
||||
let hasKubeEnv = process.env["KUBERNETES_PORT"] !== undefined;
|
||||
return fs.existsSync(workDir) && hasKubeEnv;
|
||||
}
|
||||
|
||||
export function sendAllowedEndpoints(endpoints: string): void {
|
||||
const startTime = Date.now();
|
||||
const allowedEndpoints = endpoints.split(" "); // endpoints are space separated
|
||||
|
||||
for (const endpoint of allowedEndpoints) {
|
||||
if (endpoint) {
|
||||
let sent = 0;
|
||||
for (let endpoint of allowedEndpoints) {
|
||||
endpoint = endpoint.trim();
|
||||
if (endpoint.length > 0) {
|
||||
let encodedEndpoint = Buffer.from(endpoint).toString("base64");
|
||||
let endpointPolicyStr = `step_policy_endpoint_${encodedEndpoint}`;
|
||||
echo(endpointPolicyStr);
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedEndpoints.length > 0) {
|
||||
applyPolicy(allowedEndpoints.length);
|
||||
if (sent > 0) {
|
||||
applyPolicy(sent);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(
|
||||
`[harden-runner] sendAllowedEndpoints completed in ${duration}ms (sent ${sent} endpoints)`
|
||||
);
|
||||
}
|
||||
|
||||
function applyPolicy(count: number): void {
|
||||
|
||||
+29
-10
@@ -4,18 +4,24 @@ import * as fs from "fs";
|
||||
|
||||
const CHECKSUMS = {
|
||||
tls: {
|
||||
amd64: "38e7ed97ced6fe0c1cf0fb5ee3b3d521dfe28d5ddf1cdca72d130c8d1b4a314e", // v1.4.2
|
||||
arm64: "f67c80cc578c996d4f882c14fcdb63df57927d907cd22f1ec65f9fa940c08cf3",
|
||||
amd64: "19c35eee1347077eb71306b122ad4a1cf83f36ef0f69fd91b0c0d79ffd0eabdd", // v1.7.10
|
||||
arm64: "f9192788e86b2e44b795f072e8cc03eec9852649609aeedac0761d3b67c991fa",
|
||||
},
|
||||
non_tls: {
|
||||
amd64: "a9f1842e3d7f3d38c143dbe8ffe1948e6c8173cd04da072d9f9d128bb400844a", // v0.13.7
|
||||
amd64: "23715f2485c16e2a2ad116abf0fe8443788c62e4f5f224c5858b0b41b591fc89", // v0.14.3
|
||||
},
|
||||
darwin: "797399a3a3f6f9c4c000a02e0d8c7b16499129c9bdc2ad9cf2a10072c10654fb", // v0.0.4
|
||||
windows: {
|
||||
amd64: "e98f8b9cf9ecf6566f1e16a470fbe4aef01610a644fd8203a1bab3ff142186c8", // v1.0.0
|
||||
},
|
||||
};
|
||||
|
||||
// verifyChecksum returns true if checksum is valid
|
||||
export function verifyChecksum(
|
||||
downloadPath: string,
|
||||
isTLS: boolean,
|
||||
variant: string
|
||||
variant: string,
|
||||
platform: string
|
||||
) {
|
||||
const fileBuffer: Buffer = fs.readFileSync(downloadPath);
|
||||
const checksum: string = crypto
|
||||
@@ -25,17 +31,30 @@ export function verifyChecksum(
|
||||
|
||||
let expectedChecksum: string = "";
|
||||
|
||||
if (isTLS) {
|
||||
expectedChecksum = CHECKSUMS["tls"][variant];
|
||||
} else {
|
||||
expectedChecksum = CHECKSUMS["non_tls"][variant];
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
expectedChecksum = isTLS
|
||||
? CHECKSUMS["tls"][variant]
|
||||
: CHECKSUMS["non_tls"][variant];
|
||||
break;
|
||||
case "darwin":
|
||||
expectedChecksum = CHECKSUMS["darwin"];
|
||||
break;
|
||||
case "win32":
|
||||
expectedChecksum = CHECKSUMS["windows"][variant];
|
||||
break;
|
||||
default:
|
||||
console.log(`Unsupported platform: ${platform}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checksum !== expectedChecksum) {
|
||||
core.setFailed(
|
||||
`Checksum verification failed, expected ${expectedChecksum} instead got ${checksum}`
|
||||
`❌ Checksum verification failed, expected ${expectedChecksum} instead got ${checksum}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
core.debug("Checksum verification passed.");
|
||||
core.info(`✅ Checksum verification passed. checksum=${checksum}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
+215
-17
@@ -1,14 +1,24 @@
|
||||
import * as fs from "fs";
|
||||
import * as cp from "child_process";
|
||||
import * as common from "./common";
|
||||
import * as cp from "child_process";
|
||||
import * as path from "path";
|
||||
import isDocker from "is-docker";
|
||||
import { isArcRunner } from "./arc-runner";
|
||||
import { isARCRunner } from "./arc-runner";
|
||||
import { isGithubHosted } from "./tls-inspect";
|
||||
import { context } from "@actions/github";
|
||||
import { isPlatformSupported, isAgentInstalled } from "./utils";
|
||||
|
||||
(async () => {
|
||||
console.log("[harden-runner] post-step");
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
console.log(common.UBUNTU_MESSAGE);
|
||||
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'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlatformSupported(process.platform)) {
|
||||
console.log(common.UNSUPPORTED_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (isGithubHosted() && isDocker()) {
|
||||
@@ -16,7 +26,7 @@ import { isGithubHosted } from "./tls-inspect";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArcRunner()) {
|
||||
if (isARCRunner()) {
|
||||
console.log(`[!] ${common.ARC_RUNNER_MESSAGE}`);
|
||||
return;
|
||||
}
|
||||
@@ -25,7 +35,7 @@ import { isGithubHosted } from "./tls-inspect";
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {
|
||||
if (process.env.STATE_customVMImage === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,6 +47,35 @@ import { isGithubHosted } from "./tls-inspect";
|
||||
return;
|
||||
}
|
||||
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
await handleLinuxCleanup();
|
||||
break;
|
||||
case "win32":
|
||||
await handleWindowsCleanup();
|
||||
break;
|
||||
case "darwin":
|
||||
await handleMacosCleanup();
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
await common.addSummary();
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
})();
|
||||
|
||||
async function handleLinuxCleanup() {
|
||||
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {
|
||||
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" })
|
||||
@@ -53,8 +92,8 @@ import { isGithubHosted } from "./tls-inspect";
|
||||
break;
|
||||
}
|
||||
await sleep(1000);
|
||||
} // The file *does* exist
|
||||
else {
|
||||
} else {
|
||||
// The file *does* exist
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -81,25 +120,184 @@ import { isGithubHosted } from "./tls-inspect";
|
||||
}
|
||||
|
||||
var disable_sudo = process.env.STATE_disableSudo;
|
||||
if (disable_sudo !== "true") {
|
||||
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
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMacosCleanup() {
|
||||
const post_event = "/opt/step-security/post_event.json";
|
||||
|
||||
if (isGithubHosted() && fs.existsSync(post_event)) {
|
||||
console.log("Post step already executed, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(post_event, JSON.stringify({ event: "post" }));
|
||||
|
||||
// if agent is installed; wait for it to create done.json
|
||||
if (isAgentInstalled(process.platform)) {
|
||||
let macDone = "/opt/step-security/done.json";
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
if (!fs.existsSync(macDone)) {
|
||||
counter++;
|
||||
if (counter > 10) {
|
||||
console.log("timed out");
|
||||
break;
|
||||
}
|
||||
await sleep(1000);
|
||||
} else {
|
||||
// The file *does* exist
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let macAgentLog = "/opt/step-security/agent.log";
|
||||
if (fs.existsSync(macAgentLog)) {
|
||||
console.log("macAgentLog:");
|
||||
var content = fs.readFileSync(macAgentLog, "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: 5000, // 5 seconds timeout
|
||||
}
|
||||
);
|
||||
console.log(logStreamOutput);
|
||||
} catch (error) {
|
||||
console.log("Warning: Could not fetch system log stream:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWindowsCleanup() {
|
||||
// windows cleanup
|
||||
const agentDir = process.env.STATE_agentDir || "C:\\agent";
|
||||
const postEventFile = path.join(agentDir, "post_event.json");
|
||||
|
||||
if (isGithubHosted() && fs.existsSync(postEventFile)) {
|
||||
console.log("Windows post step already executed, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.arch === "arm64") {
|
||||
console.log(common.ARM64_WINDOWS_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
const p = cp.spawn(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"query user; exit $LASTEXITCODE",
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "pipe"], shell: false, windowsHide: true }
|
||||
);
|
||||
p.unref();
|
||||
|
||||
fs.writeFileSync(postEventFile, JSON.stringify({ event: "post" }));
|
||||
|
||||
// if agent is installed; wait for it to create done.json
|
||||
if (isAgentInstalled(process.platform)) {
|
||||
const doneFile = path.join(agentDir, "done.json");
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
if (!fs.existsSync(doneFile)) {
|
||||
counter++;
|
||||
if (counter > 10) {
|
||||
console.log("timed out");
|
||||
break;
|
||||
}
|
||||
await sleep(1000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("stopping windows agent process...");
|
||||
const pidFile = path.join(agentDir, "agent.pid");
|
||||
|
||||
try {
|
||||
await common.addSummary();
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
if (!fs.existsSync(pidFile)) {
|
||||
console.log("PID file not found. Agent may not be running.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim());
|
||||
console.log(`agent PID from file: ${pid}`);
|
||||
|
||||
try {
|
||||
process.kill(pid, 0); // signal 0 just checks if process exists
|
||||
} catch {
|
||||
console.log("agent process not running.");
|
||||
fs.unlinkSync(pidFile);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`stopping agent process (PID: ${pid})...`);
|
||||
process.kill(pid, "SIGINT");
|
||||
|
||||
let gracefulShutdown = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sleep(1000);
|
||||
|
||||
try {
|
||||
process.kill(pid, 0); // check if still exists
|
||||
} catch {
|
||||
gracefulShutdown = true;
|
||||
console.log("agent process stopped gracefully");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gracefulShutdown) {
|
||||
console.log("graceful shutdown timeout (10s), forcing termination...");
|
||||
process.kill(pid, "SIGKILL");
|
||||
console.log("agent process terminated forcefully");
|
||||
}
|
||||
|
||||
if (fs.existsSync(pidFile)) {
|
||||
fs.unlinkSync(pidFile);
|
||||
console.log("PID file cleaned up");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("warning: error stopping agent process:", error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
const log = path.join(agentDir, "agent.log");
|
||||
if (fs.existsSync(log)) {
|
||||
console.log("agent log:");
|
||||
const content = fs.readFileSync(log, "utf-8");
|
||||
console.log(content);
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
+37
-73
@@ -1,6 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as fs from "fs";
|
||||
import { STEPSECURITY_WEB_URL } from "./configs";
|
||||
import { STEPSECURITY_API_URL, STEPSECURITY_WEB_URL } from "./configs";
|
||||
import { getAnnotationLogs } from "./utils";
|
||||
|
||||
export function printInfo(web_url) {
|
||||
console.log(
|
||||
@@ -59,18 +59,19 @@ export async function addSummary() {
|
||||
return;
|
||||
}
|
||||
|
||||
const web_url = STEPSECURITY_WEB_URL;
|
||||
const insights_url = `${web_url}/github/${process.env["GITHUB_REPOSITORY"]}/actions/runs/${process.env["GITHUB_RUN_ID"]}`;
|
||||
|
||||
const log = "/home/agent/agent.log";
|
||||
if (!fs.existsSync(log)) {
|
||||
const correlation_id = process.env.STATE_correlation_id;
|
||||
if (!correlation_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let needsSubscription = false;
|
||||
try {
|
||||
let data = fs.readFileSync("/home/agent/annotation.log", "utf8");
|
||||
if (data.includes("StepSecurity Harden Runner is disabled")) {
|
||||
let data = getAnnotationLogs(process.platform);
|
||||
if (
|
||||
data !== undefined &&
|
||||
data.includes("StepSecurity Harden Runner is disabled")
|
||||
) {
|
||||
needsSubscription = true;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -96,85 +97,45 @@ export async function addSummary() {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(log, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
// 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"];
|
||||
|
||||
let tableEntries = [];
|
||||
|
||||
for (const line of lines) {
|
||||
processLogLine(line, tableEntries);
|
||||
}
|
||||
|
||||
if (tableEntries.length === 0) {
|
||||
if (!owner || !repo || !run_id || !correlation_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const insightsRow = `<p><b><a href="${insights_url}">📄 View Full Report</a></b></p>`;
|
||||
// Fetch job summary from API
|
||||
const apiUrl = `${STEPSECURITY_API_URL}/github/${owner}/${repo}/actions/runs/${run_id}/correlation/${correlation_id}/job-markdown-summary`;
|
||||
|
||||
await core.summary.addSeparator().addRaw(`<h2>🛡 StepSecurity Report</h2>`);
|
||||
|
||||
tableEntries.sort((a, b) => {
|
||||
if (a.status === "❌ Blocked" && b.status !== "❌ Blocked") {
|
||||
return -1;
|
||||
} else if (a.status !== "❌ Blocked" && b.status === "❌ Blocked") {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to fetch job summary: ${response.status} ${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
tableEntries = tableEntries.slice(0, 3);
|
||||
const markdownSummary = await response.text();
|
||||
|
||||
await core.summary.addRaw(`
|
||||
<blockquote>
|
||||
<p>Preview of the outbound network calls during this workflow run.</p></blockquote>
|
||||
<h3>Network Calls</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Process</th>
|
||||
<th>Destination</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tableEntries
|
||||
.map(
|
||||
(entry) => `<tr>
|
||||
<td><code>${entry.process}</code></td>
|
||||
<td>${entry.domain.replace(/\.$/, "")}</td>
|
||||
<td>${entry.status}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")}
|
||||
<tr>
|
||||
<td><code>...</code></td>
|
||||
<td><code>...</code></td>
|
||||
<td><code>...</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
${insightsRow}
|
||||
`);
|
||||
|
||||
await core.summary
|
||||
.addRaw(
|
||||
`<p><i>Markdown generated by the <a href="https://github.com/step-security/harden-runner">Harden-Runner GitHub Action</a>.</i></p>`
|
||||
)
|
||||
.addSeparator()
|
||||
.write();
|
||||
// Render the markdown summary using core.summary.addRaw
|
||||
await core.summary.addRaw(markdownSummary).write();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job summary: ${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const STATUS_HARDEN_RUNNER_UNAVAILABLE = "409";
|
||||
|
||||
export const CONTAINER_MESSAGE =
|
||||
"This job is running in a container. Harden Runner does not run in a container as it needs sudo access to run. This job will not be monitored.";
|
||||
"This job is running in a container. Such jobs can be monitored by installing Harden Runner in a custom VM image for GitHub-hosted runners.";
|
||||
|
||||
export const UBUNTU_MESSAGE =
|
||||
"This job is not running in a GitHub Actions Hosted Runner Ubuntu VM. Harden Runner is only supported on Ubuntu VM. This job will not be monitored.";
|
||||
export const UNSUPPORTED_RUNNER_MESSAGE =
|
||||
"This job is not running in a GitHub Actions Hosted Runner. Harden Runner is only supported on GitHub-hosted runners (Ubuntu, Windows, and macOS). This job will not be monitored.";
|
||||
|
||||
export const SELF_HOSTED_NO_AGENT_MESSAGE =
|
||||
"This job is running on a self-hosted runner, but the runner does not have Harden-Runner installed. This job will not be monitored.";
|
||||
export const SELF_HOSTED_RUNNER_MESSAGE =
|
||||
"This job is running on a self-hosted runner.";
|
||||
|
||||
export const HARDEN_RUNNER_UNAVAILABLE_MESSAGE =
|
||||
"Sorry, we are currently experiencing issues with the Harden Runner installation process. It is currently unavailable.";
|
||||
@@ -184,3 +145,6 @@ 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.";
|
||||
|
||||
+11
-2
@@ -3,11 +3,20 @@ import * as core from "@actions/core";
|
||||
import isDocker from "is-docker";
|
||||
import { STEPSECURITY_WEB_URL } from "./configs";
|
||||
import { isGithubHosted } from "./tls-inspect";
|
||||
import { context } from "@actions/github";
|
||||
import { isPlatformSupported } from "./utils";
|
||||
|
||||
(async () => {
|
||||
console.log("[harden-runner] main-step");
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
console.log(common.UBUNTU_MESSAGE);
|
||||
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'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlatformSupported(process.platform)) {
|
||||
console.log(common.UNSUPPORTED_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (isGithubHosted() && isDocker()) {
|
||||
|
||||
+163
-4
@@ -5,7 +5,8 @@ import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { verifyChecksum } from "./checksum";
|
||||
import { EOL } from "os";
|
||||
import { ARM64_RUNNER_MESSAGE } from "./common";
|
||||
import { ARM64_RUNNER_MESSAGE, ARM64_WINDOWS_RUNNER_MESSAGE } from "./common";
|
||||
import { chownForFolder } from "./utils";
|
||||
|
||||
export async function installAgent(
|
||||
isTLS: boolean,
|
||||
@@ -25,7 +26,9 @@ export async function installAgent(
|
||||
|
||||
if (isTLS) {
|
||||
downloadPath = await tc.downloadTool(
|
||||
`https://packages.stepsecurity.io/github-hosted/harden-runner_1.4.2_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
|
||||
);
|
||||
} else {
|
||||
if (variant === "arm64") {
|
||||
@@ -33,13 +36,15 @@ export async function installAgent(
|
||||
return false;
|
||||
}
|
||||
downloadPath = await tc.downloadTool(
|
||||
"https://github.com/step-security/agent/releases/download/v0.13.7/agent_0.13.7_linux_amd64.tar.gz",
|
||||
"https://github.com/step-security/agent/releases/download/v0.14.3/agent_0.14.3_linux_amd64.tar.gz",
|
||||
undefined,
|
||||
auth
|
||||
);
|
||||
}
|
||||
|
||||
verifyChecksum(downloadPath, isTLS, variant);
|
||||
if (!verifyChecksum(downloadPath, isTLS, variant, "linux")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extractPath = await tc.extractTar(downloadPath);
|
||||
|
||||
@@ -63,3 +68,157 @@ export async function installAgent(
|
||||
cp.execSync("sudo service agent start", { timeout: 15000 });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function installMacosAgent(configStr: string): Promise<boolean> {
|
||||
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", configStr);
|
||||
core.info(
|
||||
"✓ Successfully created agent.json at /opt/step-security/agent.json"
|
||||
);
|
||||
|
||||
// Download installer package
|
||||
const downloadUrl =
|
||||
"https://github.com/step-security/agent-releases/releases/download/v0.0.4-mac/macos-installer-0.0.4.tar.gz";
|
||||
core.info(`Downloading macOS installer.. : ${downloadUrl}`);
|
||||
const downloadPath = await tc.downloadTool(downloadUrl, undefined, auth);
|
||||
core.info(`✓ Successfully downloaded installer to: ${downloadPath}`);
|
||||
|
||||
// Verify SHA256 checksum
|
||||
core.info("Verifying SHA256 checksum of downloaded tar file...");
|
||||
if (!verifyChecksum(downloadPath, false, "", "darwin")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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");
|
||||
const installerBinaryPath = "/opt/step-security/Installer";
|
||||
|
||||
core.info(
|
||||
`Copying Installer from ${installerSourcePath} to /opt/step-security...`
|
||||
);
|
||||
cp.execFileSync("cp", [installerSourcePath, installerBinaryPath]);
|
||||
core.info("✓ Successfully copied Installer to /opt/step-security");
|
||||
|
||||
|
||||
// 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: 10000, // 10 second timeout
|
||||
}
|
||||
);
|
||||
core.info("✓ Installer completed successfully");
|
||||
|
||||
core.info("✅ macOS agent installation 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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function installWindowsAgent(configStr: string): Promise<boolean> {
|
||||
const token = core.getInput("token", { required: true });
|
||||
const auth = `token ${token}`;
|
||||
|
||||
const variant = process.arch === "x64" ? "amd64" : "arm64";
|
||||
if (variant === "arm64") {
|
||||
console.log(ARM64_WINDOWS_RUNNER_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
const agentDir = "C:\\agent";
|
||||
core.info(`Creating agent directory: ${agentDir}`);
|
||||
if (!fs.existsSync(agentDir)) {
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `agentDir=${agentDir}${EOL}`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
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`,
|
||||
undefined,
|
||||
auth
|
||||
);
|
||||
|
||||
// validate the checksum
|
||||
if (!verifyChecksum(downloadPath, false, variant, process.platform)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extractPath = await tc.extractTar(downloadPath);
|
||||
|
||||
const extractedAgentPath = path.join(extractPath, "agent.exe");
|
||||
fs.copyFileSync(extractedAgentPath, agentExePath);
|
||||
core.info(`Copied agent from ${extractedAgentPath} to ${agentExePath}`);
|
||||
|
||||
const configPath = path.join(agentDir, "config.json");
|
||||
fs.writeFileSync(configPath, configStr);
|
||||
core.info(`Created config file: ${configPath}`);
|
||||
|
||||
core.info("Starting Windows Agent...");
|
||||
|
||||
try {
|
||||
const logPath = path.join(agentDir, "agent.log");
|
||||
const logStream = fs.openSync(logPath, "a");
|
||||
core.info(`Agent logs will be written to: ${logPath}`);
|
||||
|
||||
const agentProcess = cp.spawn(agentExePath, [], {
|
||||
cwd: agentDir,
|
||||
detached: true,
|
||||
stdio: ["ignore", logStream, logStream],
|
||||
windowsHide: false,
|
||||
shell: false,
|
||||
});
|
||||
|
||||
const pidFile = path.join(agentDir, "agent.pid");
|
||||
fs.writeFileSync(pidFile, agentProcess.pid.toString());
|
||||
core.info(`Agent process started with PID: ${agentProcess.pid}`);
|
||||
core.info(`PID saved to: ${pidFile}`);
|
||||
|
||||
agentProcess.unref();
|
||||
|
||||
core.info("Windows Agent process started successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Failed to start Windows agent process: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Configuration {
|
||||
egress_policy: string;
|
||||
disable_telemetry: boolean;
|
||||
disable_sudo: boolean;
|
||||
disable_sudo_and_containers: boolean;
|
||||
disable_file_monitoring: boolean;
|
||||
is_github_hosted: boolean;
|
||||
private: string;
|
||||
@@ -20,6 +21,7 @@ export interface PolicyResponse {
|
||||
policyName?: string;
|
||||
allowed_endpoints?: string[];
|
||||
disable_sudo?: boolean;
|
||||
disable_sudo_and_containers?: boolean;
|
||||
disable_file_monitoring?: boolean;
|
||||
disable_telemetry?: boolean;
|
||||
egress_policy?: string;
|
||||
|
||||
@@ -36,6 +36,7 @@ test("merge configs", async () => {
|
||||
egress_policy: "audit",
|
||||
disable_telemetry: false,
|
||||
disable_sudo: false,
|
||||
disable_sudo_and_containers: false,
|
||||
disable_file_monitoring: false,
|
||||
private: "true",
|
||||
is_github_hosted: true,
|
||||
@@ -62,6 +63,7 @@ test("merge configs", async () => {
|
||||
egress_policy: "audit",
|
||||
disable_telemetry: false,
|
||||
disable_sudo: false,
|
||||
disable_sudo_and_containers: false,
|
||||
disable_file_monitoring: false,
|
||||
private: "true",
|
||||
is_github_hosted: true,
|
||||
|
||||
+11
-2
@@ -39,7 +39,12 @@ export async function fetchPolicy(
|
||||
}
|
||||
|
||||
if (response === undefined && err !== undefined) {
|
||||
throw new Error(`[Policy Fetch] ${err}`);
|
||||
// Preserve the original error's statusCode if it exists
|
||||
const error = new Error(`[Policy Fetch] ${err}`);
|
||||
if (err.statusCode !== undefined) {
|
||||
(error as any).statusCode = err.statusCode;
|
||||
}
|
||||
throw error;
|
||||
} else {
|
||||
return response.result;
|
||||
}
|
||||
@@ -56,6 +61,10 @@ export function mergeConfigs(
|
||||
localConfig.disable_sudo = remoteConfig.disable_sudo;
|
||||
}
|
||||
|
||||
if (remoteConfig.disable_sudo_and_containers !== undefined) {
|
||||
localConfig.disable_sudo_and_containers = remoteConfig.disable_sudo_and_containers;
|
||||
}
|
||||
|
||||
if (remoteConfig.disable_file_monitoring !== undefined) {
|
||||
localConfig.disable_file_monitoring = remoteConfig.disable_file_monitoring;
|
||||
}
|
||||
@@ -66,7 +75,7 @@ export function mergeConfigs(
|
||||
return localConfig;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
+184
-58
@@ -19,11 +19,21 @@ import { Configuration, PolicyResponse } from "./interfaces";
|
||||
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";
|
||||
import { GetCacheEntryDownloadURLRequest } from "@actions/cache/lib/generated/results/api/v1/cache";
|
||||
import { getCacheServiceVersion } from "@actions/cache/lib/internal/config";
|
||||
|
||||
import * as utils from "@actions/cache/lib/internal/cacheUtils";
|
||||
import { isArcRunner, sendAllowedEndpoints } from "./arc-runner";
|
||||
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,
|
||||
installWindowsAgent,
|
||||
} from "./install-agent";
|
||||
|
||||
import { chownForFolder, isAgentInstalled, isPlatformSupported } from "./utils";
|
||||
|
||||
interface MonitorResponse {
|
||||
runner_ip_address?: string;
|
||||
@@ -35,8 +45,14 @@ interface MonitorResponse {
|
||||
try {
|
||||
console.log("[harden-runner] pre-step");
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
console.log(common.UBUNTU_MESSAGE);
|
||||
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'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlatformSupported(process.platform)) {
|
||||
console.log(common.UNSUPPORTED_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (isGithubHosted() && isDocker()) {
|
||||
@@ -58,6 +74,9 @@ interface MonitorResponse {
|
||||
egress_policy: core.getInput("egress-policy"),
|
||||
disable_telemetry: core.getBooleanInput("disable-telemetry"),
|
||||
disable_sudo: core.getBooleanInput("disable-sudo"),
|
||||
disable_sudo_and_containers: core.getBooleanInput(
|
||||
"disable-sudo-and-containers"
|
||||
),
|
||||
disable_file_monitoring: core.getBooleanInput("disable-file-monitoring"),
|
||||
private: context?.payload?.repository?.private || false,
|
||||
is_github_hosted: isGithubHosted(),
|
||||
@@ -78,7 +97,18 @@ interface MonitorResponse {
|
||||
confg = mergeConfigs(confg, result);
|
||||
} catch (err) {
|
||||
core.info(`[!] ${err}`);
|
||||
core.setFailed(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.');
|
||||
} else {
|
||||
// Handle different HTTP status codes
|
||||
if (err.statusCode >= 400 && err.statusCode < 500) {
|
||||
core.error('Policy not found');
|
||||
} else {
|
||||
core.error(`Unexpected error occurred: ${err}. Falling back to egress policy audit`);
|
||||
confg.egress_policy = 'audit';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.appendFileSync(
|
||||
@@ -88,6 +118,13 @@ interface MonitorResponse {
|
||||
encoding: "utf8",
|
||||
}
|
||||
);
|
||||
fs.appendFileSync(
|
||||
process.env.GITHUB_STATE,
|
||||
`disableSudoAndContainers=${confg.disable_sudo_and_containers}${EOL}`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
}
|
||||
);
|
||||
core.info(`[!] Current Configuration: \n${JSON.stringify(confg)}\n`);
|
||||
|
||||
if (confg.egress_policy !== "audit" && confg.egress_policy !== "block") {
|
||||
@@ -114,28 +151,78 @@ interface MonitorResponse {
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
try {
|
||||
const compressionMethod: CompressionMethod =
|
||||
await utils.getCompressionMethod();
|
||||
const cacheFilePath = path.join(__dirname, "cache.txt");
|
||||
core.info(`cacheFilePath ${cacheFilePath}`);
|
||||
const cacheEntry: ArtifactCacheEntry = await getCacheEntry(
|
||||
[cacheKey],
|
||||
[cacheFilePath],
|
||||
{
|
||||
compressionMethod: compressionMethod,
|
||||
|
||||
const cacheServiceVersion: string = getCacheServiceVersion();
|
||||
|
||||
switch (cacheServiceVersion) {
|
||||
case "v2":
|
||||
core.info(`cache version: v2`);
|
||||
try {
|
||||
const cacheFilePath = path.join(__dirname, "cache.txt");
|
||||
core.info(`cacheFilePath ${cacheFilePath}`);
|
||||
|
||||
const twirpClient = cacheTwirpClient.internalCacheTwirpClient();
|
||||
const compressionMethod = await utils.getCompressionMethod();
|
||||
|
||||
const request: GetCacheEntryDownloadURLRequest = {
|
||||
key: cacheKey,
|
||||
restoreKeys: [],
|
||||
version: utils.getCacheVersion(
|
||||
[cacheFilePath],
|
||||
compressionMethod,
|
||||
false
|
||||
),
|
||||
};
|
||||
|
||||
const response = await twirpClient.GetCacheEntryDownloadURL(
|
||||
request
|
||||
);
|
||||
|
||||
const url = new URL(response.signedDownloadUrl);
|
||||
core.info(
|
||||
`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`
|
||||
);
|
||||
|
||||
confg.allowed_endpoints += ` ${url.hostname}:443`;
|
||||
} catch (e) {
|
||||
core.info(`Unable to fetch cacheURL ${e}`);
|
||||
if (confg.egress_policy === "block") {
|
||||
core.info("Switching egress-policy to audit mode");
|
||||
confg.egress_policy = "audit";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "v1":
|
||||
core.info(`cache version: v1`);
|
||||
|
||||
try {
|
||||
const compressionMethod: CompressionMethod =
|
||||
await utils.getCompressionMethod();
|
||||
const cacheFilePath = path.join(__dirname, "cache.txt");
|
||||
core.info(`cacheFilePath ${cacheFilePath}`);
|
||||
|
||||
const cacheEntry: ArtifactCacheEntry = await getCacheEntry(
|
||||
[cacheKey],
|
||||
[cacheFilePath],
|
||||
{
|
||||
compressionMethod: compressionMethod,
|
||||
}
|
||||
);
|
||||
const url = new URL(cacheEntry.archiveLocation);
|
||||
core.info(
|
||||
`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`
|
||||
);
|
||||
|
||||
confg.allowed_endpoints += ` ${url.hostname}:443`;
|
||||
} catch (exception) {
|
||||
// some exception has occurred.
|
||||
core.info(`Unable to fetch cacheURL ${exception}`);
|
||||
if (confg.egress_policy === "block") {
|
||||
core.info("Switching egress-policy to audit mode");
|
||||
confg.egress_policy = "audit";
|
||||
}
|
||||
}
|
||||
);
|
||||
const url = new URL(cacheEntry.archiveLocation);
|
||||
core.info(`Adding cacheHost: ${url.hostname}:443 to allowed-endpoints`);
|
||||
confg.allowed_endpoints += ` ${url.hostname}:443`;
|
||||
} catch (exception) {
|
||||
// some exception has occurred.
|
||||
core.info(`Unable to fetch cacheURL`);
|
||||
if (confg.egress_policy === "block") {
|
||||
core.info("Switching egress-policy to audit mode");
|
||||
confg.egress_policy = "audit";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +230,7 @@ interface MonitorResponse {
|
||||
common.printInfo(web_url);
|
||||
}
|
||||
|
||||
if (isArcRunner()) {
|
||||
if (isARCRunner()) {
|
||||
console.log(`[!] ${common.ARC_RUNNER_MESSAGE}`);
|
||||
if (confg.egress_policy === "block") {
|
||||
sendAllowedEndpoints(confg.allowed_endpoints);
|
||||
@@ -158,28 +245,37 @@ interface MonitorResponse {
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `selfHosted=true${EOL}`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (!fs.existsSync("/home/agent/agent")) {
|
||||
core.info(common.SELF_HOSTED_NO_AGENT_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (confg.egress_policy === "block") {
|
||||
try {
|
||||
if (process.env.USER) {
|
||||
chownForFolder(process.env.USER, "/home/agent");
|
||||
}
|
||||
|
||||
const confgStr = JSON.stringify(confg);
|
||||
fs.writeFileSync("/home/agent/block_event.json", confgStr);
|
||||
await sleep(5000);
|
||||
} catch (error) {
|
||||
core.info(`[!] Unable to write block_event.json: ${error}`);
|
||||
}
|
||||
core.info(common.SELF_HOSTED_RUNNER_MESSAGE);
|
||||
|
||||
if (confg.egress_policy === "block") {
|
||||
sendAllowedEndpoints(confg.allowed_endpoints);
|
||||
await sleep(5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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.");
|
||||
|
||||
if (confg.egress_policy === "block") {
|
||||
sendAllowedEndpoints(confg.allowed_endpoints);
|
||||
await sleep(5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGithubHosted() && isAgentInstalled(process.platform)) {
|
||||
console.log("Agent already installed, skipping installation");
|
||||
return;
|
||||
}
|
||||
|
||||
let _http = new httpm.HttpClient();
|
||||
let statusCode;
|
||||
let statusCode: number | undefined;
|
||||
_http.requestOptions = { socketTimeout: 3 * 1000 };
|
||||
let addSummary = "false";
|
||||
try {
|
||||
@@ -217,6 +313,13 @@ interface MonitorResponse {
|
||||
encoding: "utf8",
|
||||
}
|
||||
);
|
||||
fs.appendFileSync(
|
||||
process.env.GITHUB_STATE,
|
||||
`correlation_id=${correlation_id}${EOL}`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Step Security Job Correlation ID: ${correlation_id}`);
|
||||
if (String(statusCode) === common.STATUS_HARDEN_RUNNER_UNAVAILABLE) {
|
||||
@@ -224,18 +327,47 @@ interface MonitorResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
const confgStr = JSON.stringify(confg);
|
||||
cp.execSync("sudo mkdir -p /home/agent");
|
||||
chownForFolder(process.env.USER, "/home/agent");
|
||||
const configStr = JSON.stringify(confg);
|
||||
|
||||
let isTLS = await isTLSEnabled(context.repo.owner);
|
||||
// platform specific
|
||||
let statusFile = "";
|
||||
let logFile = "";
|
||||
let agentInstalled = false;
|
||||
|
||||
const agentInstalled = await installAgent(isTLS, confgStr);
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
statusFile = "/home/agent/agent.status";
|
||||
logFile = "/home/agent/agent.log";
|
||||
|
||||
cp.execSync("sudo mkdir -p /home/agent");
|
||||
chownForFolder(process.env.USER, "/home/agent");
|
||||
|
||||
let isTLS = await isTLSEnabled(context.repo.owner);
|
||||
agentInstalled = await installAgent(isTLS, configStr);
|
||||
|
||||
break;
|
||||
case "win32":
|
||||
core.info("Installing Windows Agent...");
|
||||
agentInstalled = await installWindowsAgent(configStr);
|
||||
|
||||
const agentDir = process.env.STATE_agentDir || "C:\\agent";
|
||||
statusFile = path.join(agentDir, "agent.status");
|
||||
logFile = path.join(agentDir, "agent.log");
|
||||
|
||||
break;
|
||||
case "darwin":
|
||||
const installed = await installMacosAgent(configStr);
|
||||
if (!installed) {
|
||||
core.warning("😭 macos agent installation failed");
|
||||
}
|
||||
return; // early return
|
||||
default:
|
||||
throw new Error(
|
||||
`Setup failed because of unsupported platform: ${process.platform}`
|
||||
);
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -265,14 +397,8 @@ interface MonitorResponse {
|
||||
process.exit(0);
|
||||
})();
|
||||
|
||||
export function sleep(ms) {
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function chownForFolder(newOwner: string, target: string) {
|
||||
let cmd = "sudo";
|
||||
let args = ["chown", "-R", newOwner, target];
|
||||
cp.execFileSync(cmd, args);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as cp from "child_process";
|
||||
import * as fs from "fs";
|
||||
|
||||
export function isPlatformSupported(platform: NodeJS.Platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
case "win32":
|
||||
case "darwin":
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function chownForFolder(newOwner: string, target: string) {
|
||||
let cmd = "sudo";
|
||||
let args = ["chown", "-R", newOwner, target];
|
||||
cp.execFileSync(cmd, args);
|
||||
}
|
||||
|
||||
export function isAgentInstalled(platform: NodeJS.Platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
return fs.existsSync("/home/agent/agent.status");
|
||||
case "win32":
|
||||
return fs.existsSync("C:\\agent\\agent.status");
|
||||
case "darwin":
|
||||
return fs.existsSync("/opt/step-security/agent.status");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAnnotationLogs(platform: NodeJS.Platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
return fs.readFileSync("/home/agent/annotation.log", "utf8");
|
||||
case "win32":
|
||||
return fs.readFileSync("C:\\agent\\annotation.log", "utf8");
|
||||
case "darwin":
|
||||
return fs.readFileSync("/opt/step-security/annotation.log", "utf8");
|
||||
default:
|
||||
throw new Error("platform not supported");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user