Compare commits
18 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9af89fc715 |
||
|
|
485dce8cb5 |
||
|
|
ab7a9404c0 |
||
|
|
ec41b783c2 |
||
|
|
9ca718d3bf |
||
|
|
1dee3df8d2 |
||
|
|
a5ad31d6a1 |
||
|
|
6e928567d7 |
||
|
|
4e0504ee08 |
||
|
|
8d3c67de8e |
||
|
|
6c3c2f2c1c |
||
|
|
376d25a97f |
||
|
|
f808768d15 |
||
|
|
fe10465874 |
||
|
|
fa2e9d605c |
||
|
|
58077d3c7e |
||
|
|
a90bcbc653 |
||
|
|
f0a59d8853 |
26 changed files with 1884 additions and 448 deletions
26
README.md
26
README.md
|
|
@ -19,19 +19,22 @@ 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. 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, with support for Linux, Windows, and macOS runners. This approach brings CI/CD runners under the same level of security scrutiny as other critical systems, addressing a significant gap in the software supply chain.
|
||||
### Harden-Runner: Security Incidents Detected
|
||||
|
||||
- [Harden-Runner Detected the Compromised axios npm Package Dropping a Remote Access Trojan](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan) ([backstage/backstage#33693](https://github.com/backstage/backstage/issues/33693), [block/elasticgraph#1103](https://github.com/block/elasticgraph/issues/1103))
|
||||
- [Harden-Runner Detected the Trivy Compromise with Malicious v0.69.4 Release](https://www.stepsecurity.io/blog/trivy-compromised-a-second-time---malicious-v0-69-4-release) ([k8gb-io/k8gb#2294](https://github.com/k8gb-io/k8gb/issues/2294))
|
||||
- [Harden-Runner Detected the tj-actions/changed-files compromise](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) ([CVE-2025-30066](https://github.com/advisories/GHSA-mrrh-fwg8-r2c3))
|
||||
- [Harden-Runner Detected a CI/CD Supply Chain Attack in Google’s Open-Source Project Flank](https://www.stepsecurity.io/case-studies/flank)
|
||||
- [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 **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:
|
||||
Harden-Runner secures over **25 million CI/CD workflow runs every week**, protecting thousands of pipelines, including those from popular open-source projects by **Microsoft, Google, and CISA**. See how top projects are using Harden-Runner and explore the insights:
|
||||
➡️ [Who's using Harden-Runner?](https://docs.stepsecurity.io/whos-using-harden-runner)
|
||||
|
||||
## Quick Links
|
||||
|
|
@ -69,7 +72,7 @@ To integrate Harden-Runner, follow these steps:
|
|||
```yaml
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
@ -137,7 +140,7 @@ Explore the full feature set in the [Features Documentation](https://docs.stepse
|
|||
|
||||
## Trusted By and Case Studies
|
||||
|
||||
Harden-Runner is trusted by over 8000 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
|
||||
|
||||
|
|
@ -148,11 +151,11 @@ Harden-Runner is trusted by over 8000 leading open-source projects and enterpris
|
|||
|
||||
### Enterprise Case Studies
|
||||
|
||||
- [How Mercari Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/mercari)
|
||||
- [How Omnissa Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/omnissa)
|
||||
- [Chainguard Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/chainguard)
|
||||
- [How Coveo Strengthened GitHub Actions Security with StepSecurity](https://www.stepsecurity.io/case-studies/coveo)
|
||||
- [Hashgraph Achieves Comprehensive CI/CD Security Without Compromising Development Speed](https://www.stepsecurity.io/case-studies/hashgraph)
|
||||
- [Chainguard Secures GitHub Actions with StepSecurity](https://www.stepsecurity.io/case-studies/chainguard)
|
||||
- [Kapiche secures their GitHub Actions software supply chain with Harden-Runner](https://www.stepsecurity.io/case-studies/kapiche)
|
||||
- [Arcjet Enhances CI/CD Security with Harden-Runner](https://www.stepsecurity.io/case-studies/arcjet)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -160,9 +163,10 @@ Harden-Runner is trusted by over 8000 leading open-source projects and enterpris
|
|||
|
||||
Harden-Runner is designed to work seamlessly across a variety of runner environments, providing consistent security insights and protections regardless of where your workflows execute. For self-hosted runners, audit mode is deployed directly to the runner infrastructure without requiring any changes to your existing workflows. For more details, refer to the [official documentation](https://docs.stepsecurity.io/harden-runner).
|
||||
|
||||
| Environment Type | Compatibility | Audit Mode Deployment | Workflow Changes for Audit Mode |
|
||||
| Environment Type | Compatibility | Audit Mode Deployment | Workflow Changes for Audit/Block Mode |
|
||||
|------------------|---------------|--------------------------|-------------------|
|
||||
| GitHub-hosted runners | ✅ Full support | Add Harden-Runner Action to workflow | Yes |
|
||||
| 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 |
|
||||
|
|
@ -176,7 +180,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).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
16
action.yml
16
action.yml
|
|
@ -29,15 +29,27 @@ inputs:
|
|||
required: false
|
||||
default: "false"
|
||||
policy:
|
||||
description: "Policy name to be used from the policy store"
|
||||
description: "Policy name to be used from the policy store. Requires id-token: write permission."
|
||||
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"
|
||||
deploy-on-self-hosted-vm:
|
||||
description: "Set to true to deploy the Harden Runner agent directly on a self-hosted runner VM (Linux only). The recommended approach for self-hosted VMs is to bake the agent into the VM image; see docs.stepsecurity.io. Use this option only if baking is not possible, and only for ephemeral runners."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
branding:
|
||||
icon: "check-square"
|
||||
color: "green"
|
||||
runs:
|
||||
using: "node20"
|
||||
using: "node24"
|
||||
pre: "dist/pre/index.js"
|
||||
main: "dist/index.js"
|
||||
post: "dist/post/index.js"
|
||||
|
|
|
|||
19
dist/index.js
vendored
19
dist/index.js
vendored
|
|
@ -31910,6 +31910,22 @@ function isAgentInstalled(platform) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
function shouldDeployAgentOnSelfHosted(deployOnSelfHostedVm, isContainer, agentAlreadyInstalled) {
|
||||
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
|
||||
}
|
||||
function detectThirdPartyRunnerProvider() {
|
||||
var _a;
|
||||
if (process.env["DEPOT_RUNNER"] === "1")
|
||||
return "depot";
|
||||
if (process.env["NAMESPACE_GITHUB_RUNTIME"])
|
||||
return "namespace";
|
||||
const runnerName = (_a = process.env["RUNNER_NAME"]) !== null && _a !== void 0 ? _a : "";
|
||||
if (runnerName.startsWith("warp-"))
|
||||
return "warp";
|
||||
if (runnerName.startsWith("blacksmith-"))
|
||||
return "blacksmith";
|
||||
return null;
|
||||
}
|
||||
function utils_getAnnotationLogs(platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
|
|
@ -32029,6 +32045,8 @@ const SELF_HOSTED_RUNNER_MESSAGE = "This job is running on a self-hosted runner.
|
|||
const HARDEN_RUNNER_UNAVAILABLE_MESSAGE = "Sorry, we are currently experiencing issues with the Harden Runner installation process. It is currently unavailable.";
|
||||
const ARC_RUNNER_MESSAGE = "Workflow is currently being executed in ARC based runner.";
|
||||
const ARM64_RUNNER_MESSAGE = "ARM runners are not supported in the Harden-Runner community tier.";
|
||||
const ARM64_WINDOWS_RUNNER_MESSAGE = "Windows ARM runners are not yet supported by Harden-Runner.";
|
||||
const UBUNTU_SLIM_MESSAGE = "This job is running on an ubuntu-slim runner. Harden Runner is not supported on ubuntu-slim runners. This job will not be monitored.";
|
||||
|
||||
;// CONCATENATED MODULE: external "node:fs"
|
||||
const external_node_fs_namespaceObject = require("node:fs");
|
||||
|
|
@ -32066,6 +32084,7 @@ function isDocker() {
|
|||
;// CONCATENATED MODULE: ./src/configs.ts
|
||||
const STEPSECURITY_ENV = "agent"; // agent or int
|
||||
const configs_STEPSECURITY_API_URL = (/* unused pure expression or super */ null && (`https://${STEPSECURITY_ENV}.api.stepsecurity.io/v1`));
|
||||
const STEPSECURITY_TELEMETRY_URL = "https://prod.app-api.stepsecurity.io/v1";
|
||||
const STEPSECURITY_WEB_URL = "https://app.stepsecurity.io";
|
||||
|
||||
// EXTERNAL MODULE: ./node_modules/@actions/http-client/lib/index.js
|
||||
|
|
|
|||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
66
dist/post/index.js
vendored
66
dist/post/index.js
vendored
|
|
@ -31881,6 +31881,7 @@ var lib_core = __nccwpck_require__(7484);
|
|||
;// CONCATENATED MODULE: ./src/configs.ts
|
||||
const STEPSECURITY_ENV = "agent"; // agent or int
|
||||
const configs_STEPSECURITY_API_URL = `https://${STEPSECURITY_ENV}.api.stepsecurity.io/v1`;
|
||||
const STEPSECURITY_TELEMETRY_URL = "https://prod.app-api.stepsecurity.io/v1";
|
||||
const STEPSECURITY_WEB_URL = "https://app.stepsecurity.io";
|
||||
|
||||
// EXTERNAL MODULE: external "child_process"
|
||||
|
|
@ -31915,6 +31916,22 @@ function isAgentInstalled(platform) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
function shouldDeployAgentOnSelfHosted(deployOnSelfHostedVm, isContainer, agentAlreadyInstalled) {
|
||||
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
|
||||
}
|
||||
function detectThirdPartyRunnerProvider() {
|
||||
var _a;
|
||||
if (process.env["DEPOT_RUNNER"] === "1")
|
||||
return "depot";
|
||||
if (process.env["NAMESPACE_GITHUB_RUNTIME"])
|
||||
return "namespace";
|
||||
const runnerName = (_a = process.env["RUNNER_NAME"]) !== null && _a !== void 0 ? _a : "";
|
||||
if (runnerName.startsWith("warp-"))
|
||||
return "warp";
|
||||
if (runnerName.startsWith("blacksmith-"))
|
||||
return "blacksmith";
|
||||
return null;
|
||||
}
|
||||
function getAnnotationLogs(platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
|
|
@ -32034,6 +32051,8 @@ const SELF_HOSTED_RUNNER_MESSAGE = "This job is running on a self-hosted runner.
|
|||
const HARDEN_RUNNER_UNAVAILABLE_MESSAGE = "Sorry, we are currently experiencing issues with the Harden Runner installation process. It is currently unavailable.";
|
||||
const ARC_RUNNER_MESSAGE = "Workflow is currently being executed in ARC based runner.";
|
||||
const ARM64_RUNNER_MESSAGE = "ARM runners are not supported in the Harden-Runner community tier.";
|
||||
const ARM64_WINDOWS_RUNNER_MESSAGE = "Windows ARM runners are not yet supported by Harden-Runner.";
|
||||
const UBUNTU_SLIM_MESSAGE = "This job is running on an ubuntu-slim runner. Harden Runner is not supported on ubuntu-slim runners. This job will not be monitored.";
|
||||
|
||||
// EXTERNAL MODULE: external "path"
|
||||
var external_path_ = __nccwpck_require__(6928);
|
||||
|
|
@ -32196,10 +32215,15 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
|
|||
console.log(CONTAINER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (isGithubHosted() && process.platform === "linux" && !process.env.USER) {
|
||||
console.log(UBUNTU_SLIM_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (isARCRunner()) {
|
||||
console.log(`[!] ${ARC_RUNNER_MESSAGE}`);
|
||||
return;
|
||||
}
|
||||
const thirdPartyProvider = detectThirdPartyRunnerProvider();
|
||||
if (process.env.STATE_selfHosted === "true") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -32213,7 +32237,12 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
|
|||
}
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
yield handleLinuxCleanup();
|
||||
if (thirdPartyProvider) {
|
||||
yield handleAgentBravoCleanup();
|
||||
}
|
||||
else {
|
||||
yield handleLinuxCleanup();
|
||||
}
|
||||
break;
|
||||
case "win32":
|
||||
yield handleWindowsCleanup();
|
||||
|
|
@ -32229,6 +32258,37 @@ var cleanup_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _
|
|||
console.log(exception);
|
||||
}
|
||||
}))();
|
||||
function handleAgentBravoCleanup() {
|
||||
return cleanup_awaiter(this, void 0, void 0, function* () {
|
||||
external_child_process_.execFileSync("/usr/bin/echo", ["step_policy_jobend"]);
|
||||
const doneFile = "/home/agent/done.json";
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
if (!external_fs_.existsSync(doneFile)) {
|
||||
counter++;
|
||||
if (counter > 10) {
|
||||
console.log("timed out");
|
||||
break;
|
||||
}
|
||||
yield sleep(1000);
|
||||
}
|
||||
else {
|
||||
console.log(external_fs_.readFileSync(doneFile, "utf-8"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const log = "/home/agent/agent.log";
|
||||
if (external_fs_.existsSync(log)) {
|
||||
console.log("log:");
|
||||
console.log(external_fs_.readFileSync(log, "utf-8"));
|
||||
}
|
||||
const status = "/home/agent/agent.status";
|
||||
if (external_fs_.existsSync(status)) {
|
||||
console.log("status:");
|
||||
console.log(external_fs_.readFileSync(status, "utf-8"));
|
||||
}
|
||||
});
|
||||
}
|
||||
function handleLinuxCleanup() {
|
||||
return cleanup_awaiter(this, void 0, void 0, function* () {
|
||||
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {
|
||||
|
|
@ -32350,6 +32410,10 @@ function handleWindowsCleanup() {
|
|||
console.log("Windows post step already executed, skipping");
|
||||
return;
|
||||
}
|
||||
if (process.arch === "arm64") {
|
||||
console.log(ARM64_WINDOWS_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
const p = external_child_process_.spawn("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
|
|
|
|||
2
dist/post/index.js.map
vendored
2
dist/post/index.js.map
vendored
File diff suppressed because one or more lines are too long
858
dist/pre/index.js
vendored
858
dist/pre/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/pre/index.js.map
vendored
2
dist/pre/index.js.map
vendored
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,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) 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.
|
||||
|
|
@ -8,4 +8,8 @@ reason = "Untrusted headers are not processed"
|
|||
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-xx4v-prfh-6cgc"
|
||||
reason = "Untrusted headers are not processed"
|
||||
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"
|
||||
210
package-lock.json
generated
210
package-lock.json
generated
|
|
@ -2673,10 +2673,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -2851,9 +2852,10 @@
|
|||
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
|
@ -3746,18 +3748,53 @@
|
|||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"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==",
|
||||
"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.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^2.0.5"
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.11.tgz",
|
||||
"integrity": "sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.4.0",
|
||||
"strnum": "^2.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
|
|
@ -3854,10 +3891,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
|
||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||
"dev": true
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.5.5",
|
||||
|
|
@ -6047,9 +6085,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -6178,9 +6216,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -6415,6 +6454,21 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
|
|
@ -6454,10 +6508,11 @@
|
|||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
|
@ -6933,15 +6988,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
|
||||
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
|
|
@ -6984,15 +7040,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/table/node_modules/ajv": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -9531,9 +9588,9 @@
|
|||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
|
|
@ -9665,9 +9722,9 @@
|
|||
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
|
@ -10329,12 +10386,28 @@
|
|||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-xml-parser": {
|
||||
"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==",
|
||||
"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.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"requires": {
|
||||
"strnum": "^2.0.5"
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"fast-xml-parser": {
|
||||
"version": "5.5.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.11.tgz",
|
||||
"integrity": "sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==",
|
||||
"requires": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.4.0",
|
||||
"strnum": "^2.2.3"
|
||||
}
|
||||
},
|
||||
"fastq": {
|
||||
|
|
@ -10403,9 +10476,9 @@
|
|||
}
|
||||
},
|
||||
"flatted": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
|
||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
|
|
@ -12042,9 +12115,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.memoize": {
|
||||
|
|
@ -12145,9 +12218,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
|
@ -12311,6 +12384,11 @@
|
|||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true
|
||||
},
|
||||
"path-expression-matcher": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
|
|
@ -12341,9 +12419,9 @@
|
|||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true
|
||||
},
|
||||
"pirates": {
|
||||
|
|
@ -12670,9 +12748,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"strnum": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
|
||||
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q=="
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
|
|
@ -12703,15 +12781,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
|
|
|
|||
81
src/bravo-config.test.ts
Normal file
81
src/bravo-config.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { buildBravoConfig } from "./bravo-config";
|
||||
import { Configuration } from "./interfaces";
|
||||
|
||||
const base: Configuration = {
|
||||
repo: "org/repo",
|
||||
run_id: "123",
|
||||
correlation_id: "depot-abc",
|
||||
working_directory: "/w",
|
||||
api_url: "https://int.api.stepsecurity.io/v1",
|
||||
telemetry_url: "https://int.app-api.stepsecurity.io/v1",
|
||||
allowed_endpoints: "github.com:443",
|
||||
egress_policy: "audit",
|
||||
disable_telemetry: false,
|
||||
disable_sudo: false,
|
||||
disable_sudo_and_containers: false,
|
||||
disable_file_monitoring: false,
|
||||
is_github_hosted: false,
|
||||
private: "true" as unknown as string,
|
||||
is_debug: false,
|
||||
one_time_key: "otk-xyz",
|
||||
api_key: "tenant-key",
|
||||
use_policy_store: false,
|
||||
deploy_on_self_hosted_vm: false,
|
||||
};
|
||||
|
||||
describe("buildBravoConfig", () => {
|
||||
test("forces is_github_hosted=true so agent honors passed correlation_id", () => {
|
||||
expect(buildBravoConfig(base).is_github_hosted).toBe(true);
|
||||
});
|
||||
|
||||
test("omits api_key (agent authenticates via one_time_key, not vm-api-key)", () => {
|
||||
expect(buildBravoConfig(base)).not.toHaveProperty("api_key");
|
||||
});
|
||||
|
||||
test("omits customer (server infers tenant from repo)", () => {
|
||||
expect(buildBravoConfig(base)).not.toHaveProperty("customer");
|
||||
});
|
||||
|
||||
test("omits use_policy_store (action-side concern, not agent)", () => {
|
||||
expect(buildBravoConfig(base)).not.toHaveProperty("use_policy_store");
|
||||
});
|
||||
|
||||
test("forwards telemetry_url so network events hit configured env", () => {
|
||||
expect(buildBravoConfig(base).telemetry_url).toBe(base.telemetry_url);
|
||||
});
|
||||
|
||||
test("forwards one_time_key so agent can auth to presigned URL endpoint", () => {
|
||||
expect(buildBravoConfig(base).one_time_key).toBe("otk-xyz");
|
||||
});
|
||||
|
||||
test("forwards repo, run_id, correlation_id so server can attribute events", () => {
|
||||
const cfg = buildBravoConfig(base);
|
||||
expect(cfg.repo).toBe("org/repo");
|
||||
expect(cfg.run_id).toBe("123");
|
||||
expect(cfg.correlation_id).toBe("depot-abc");
|
||||
});
|
||||
|
||||
test("forwards private flag", () => {
|
||||
expect(buildBravoConfig(base).private).toBe(base.private);
|
||||
});
|
||||
|
||||
test("forwards egress_policy and allowed_endpoints", () => {
|
||||
const cfg = buildBravoConfig(base);
|
||||
expect(cfg.egress_policy).toBe("audit");
|
||||
expect(cfg.allowed_endpoints).toBe("github.com:443");
|
||||
});
|
||||
|
||||
test("forwards disable_* flags", () => {
|
||||
const cfg = buildBravoConfig({
|
||||
...base,
|
||||
disable_telemetry: true,
|
||||
disable_sudo: true,
|
||||
disable_sudo_and_containers: true,
|
||||
disable_file_monitoring: true,
|
||||
});
|
||||
expect(cfg.disable_telemetry).toBe(true);
|
||||
expect(cfg.disable_sudo).toBe(true);
|
||||
expect(cfg.disable_sudo_and_containers).toBe(true);
|
||||
expect(cfg.disable_file_monitoring).toBe(true);
|
||||
});
|
||||
});
|
||||
21
src/bravo-config.ts
Normal file
21
src/bravo-config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Configuration } from "./interfaces";
|
||||
|
||||
export function buildBravoConfig(confg: Configuration) {
|
||||
return {
|
||||
repo: confg.repo,
|
||||
run_id: confg.run_id,
|
||||
correlation_id: confg.correlation_id,
|
||||
working_directory: confg.working_directory,
|
||||
api_url: confg.api_url,
|
||||
telemetry_url: confg.telemetry_url,
|
||||
one_time_key: confg.one_time_key,
|
||||
allowed_endpoints: confg.allowed_endpoints,
|
||||
egress_policy: confg.egress_policy,
|
||||
disable_telemetry: confg.disable_telemetry,
|
||||
disable_sudo: confg.disable_sudo,
|
||||
disable_sudo_and_containers: confg.disable_sudo_and_containers,
|
||||
disable_file_monitoring: confg.disable_file_monitoring,
|
||||
private: confg.private,
|
||||
is_github_hosted: true,
|
||||
};
|
||||
}
|
||||
98
src/checksum.test.ts
Normal file
98
src/checksum.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import * as fs from "fs";
|
||||
import * as crypto from "crypto";
|
||||
import * as core from "@actions/core";
|
||||
import { verifyChecksum, CHECKSUMS } from "./checksum";
|
||||
|
||||
jest.mock("fs", () => ({
|
||||
...jest.requireActual("fs"),
|
||||
readFileSync: jest.fn(),
|
||||
}));
|
||||
jest.mock("crypto", () => ({
|
||||
...jest.requireActual("crypto"),
|
||||
createHash: jest.fn(),
|
||||
}));
|
||||
jest.mock("@actions/core");
|
||||
|
||||
const mockReadFile = fs.readFileSync as jest.MockedFunction<typeof fs.readFileSync>;
|
||||
const mockSetFailed = core.setFailed as jest.MockedFunction<typeof core.setFailed>;
|
||||
const mockCreateHash = crypto.createHash as jest.MockedFunction<typeof crypto.createHash>;
|
||||
|
||||
function stubHash(hash: string) {
|
||||
mockCreateHash.mockReturnValue({
|
||||
update: jest.fn().mockReturnThis(),
|
||||
digest: jest.fn().mockReturnValue(hash),
|
||||
} as unknown as crypto.Hash);
|
||||
}
|
||||
|
||||
const WRONG_HASH = "0".repeat(64);
|
||||
|
||||
describe("verifyChecksum", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockReadFile.mockReturnValue(Buffer.from("test-payload"));
|
||||
});
|
||||
|
||||
describe("agentType=bravo", () => {
|
||||
test("passes with matching bravo amd64 checksum", () => {
|
||||
stubHash(CHECKSUMS.bravo.amd64);
|
||||
expect(verifyChecksum("/tmp/f", true, "amd64", "linux", "bravo")).toBe(true);
|
||||
expect(mockSetFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("passes with matching bravo arm64 checksum", () => {
|
||||
stubHash(CHECKSUMS.bravo.arm64);
|
||||
expect(verifyChecksum("/tmp/f", true, "arm64", "linux", "bravo")).toBe(true);
|
||||
});
|
||||
|
||||
test("uses bravo checksum even when isTLS=false", () => {
|
||||
stubHash(CHECKSUMS.bravo.amd64);
|
||||
expect(verifyChecksum("/tmp/f", false, "amd64", "linux", "bravo")).toBe(true);
|
||||
});
|
||||
|
||||
test("fails on mismatched bravo checksum", () => {
|
||||
stubHash(WRONG_HASH);
|
||||
expect(verifyChecksum("/tmp/f", true, "amd64", "linux", "bravo")).toBe(false);
|
||||
expect(mockSetFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("agentType default (omitted)", () => {
|
||||
test("uses TLS checksum when isTLS=true", () => {
|
||||
stubHash(CHECKSUMS.tls.amd64);
|
||||
expect(verifyChecksum("/tmp/f", true, "amd64", "linux")).toBe(true);
|
||||
});
|
||||
|
||||
test("uses non_tls checksum when isTLS=false", () => {
|
||||
stubHash(CHECKSUMS.non_tls.amd64);
|
||||
expect(verifyChecksum("/tmp/f", false, "amd64", "linux")).toBe(true);
|
||||
});
|
||||
|
||||
test("TLS mismatch fails", () => {
|
||||
stubHash(CHECKSUMS.bravo.amd64);
|
||||
expect(verifyChecksum("/tmp/f", true, "amd64", "linux")).toBe(false);
|
||||
expect(mockSetFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("darwin", () => {
|
||||
test("passes with matching darwin checksum", () => {
|
||||
stubHash(CHECKSUMS.darwin);
|
||||
expect(verifyChecksum("/tmp/f", false, "", "darwin")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("win32", () => {
|
||||
test("passes with matching windows amd64 checksum", () => {
|
||||
stubHash(CHECKSUMS.windows.amd64);
|
||||
expect(verifyChecksum("/tmp/f", false, "amd64", "win32")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported platform", () => {
|
||||
test("returns false without calling setFailed", () => {
|
||||
stubHash(CHECKSUMS.bravo.amd64);
|
||||
expect(verifyChecksum("/tmp/f", true, "amd64", "freebsd")).toBe(false);
|
||||
expect(mockSetFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,17 +2,21 @@ import * as core from "@actions/core";
|
|||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
|
||||
const CHECKSUMS = {
|
||||
export const CHECKSUMS = {
|
||||
tls: {
|
||||
amd64: "19c35eee1347077eb71306b122ad4a1cf83f36ef0f69fd91b0c0d79ffd0eabdd", // v1.7.10
|
||||
arm64: "f9192788e86b2e44b795f072e8cc03eec9852649609aeedac0761d3b67c991fa",
|
||||
amd64: "d58a9c1c5245155ce4c71507a61e213a29925a7c39c0d20bfd00bef0d281bdbb", // v1.8.6
|
||||
arm64: "084fa95e74d17321dd1c37c93abeb8577e53ddf5266410e19f52aa79a02ae33e",
|
||||
},
|
||||
non_tls: {
|
||||
amd64: "23715f2485c16e2a2ad116abf0fe8443788c62e4f5f224c5858b0b41b591fc89", // v0.14.3
|
||||
amd64: "e38de61e1afd98dd339bb9acce4996183875d482be1638fb198ab02b3e25bbef", // v0.16.0
|
||||
},
|
||||
darwin: "797399a3a3f6f9c4c000a02e0d8c7b16499129c9bdc2ad9cf2a10072c10654fb", // v0.0.4
|
||||
bravo: {
|
||||
amd64: "495f607a891d89f12214849301f247bdca565afe67deb170fe7e5d6d361852ca", // v1.8.6
|
||||
arm64: "f96f66ab946097aae1fc887e12fe1cefcc5d510bce179221c7185374e4adf538",
|
||||
},
|
||||
darwin: "fe26a1f6af4afe9f1a854d8633832f5d18ab542827003cae445b3a64021d612c", // v0.0.5
|
||||
windows: {
|
||||
amd64: "e98f8b9cf9ecf6566f1e16a470fbe4aef01610a644fd8203a1bab3ff142186c8", // v1.0.0
|
||||
amd64: "93f1e5d87c6647e6eca7963d5f4b4bd73107029430f8e6945ffece93007a89f5", // v1.0.2
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -21,7 +25,8 @@ export function verifyChecksum(
|
|||
downloadPath: string,
|
||||
isTLS: boolean,
|
||||
variant: string,
|
||||
platform: string
|
||||
platform: string,
|
||||
agentType: "default" | "bravo" = "default"
|
||||
) {
|
||||
const fileBuffer: Buffer = fs.readFileSync(downloadPath);
|
||||
const checksum: string = crypto
|
||||
|
|
@ -33,9 +38,13 @@ export function verifyChecksum(
|
|||
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
expectedChecksum = isTLS
|
||||
? CHECKSUMS["tls"][variant]
|
||||
: CHECKSUMS["non_tls"][variant];
|
||||
if (agentType === "bravo") {
|
||||
expectedChecksum = CHECKSUMS["bravo"][variant];
|
||||
} else {
|
||||
expectedChecksum = isTLS
|
||||
? CHECKSUMS["tls"][variant]
|
||||
: CHECKSUMS["non_tls"][variant];
|
||||
}
|
||||
break;
|
||||
case "darwin":
|
||||
expectedChecksum = CHECKSUMS["darwin"];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import isDocker from "is-docker";
|
|||
import { isARCRunner } from "./arc-runner";
|
||||
import { isGithubHosted } from "./tls-inspect";
|
||||
import { context } from "@actions/github";
|
||||
import { isPlatformSupported, isAgentInstalled } from "./utils";
|
||||
import { isPlatformSupported, isAgentInstalled, detectThirdPartyRunnerProvider } from "./utils";
|
||||
|
||||
(async () => {
|
||||
console.log("[harden-runner] post-step");
|
||||
|
|
@ -26,11 +26,18 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
|
|||
return;
|
||||
}
|
||||
|
||||
if (isGithubHosted() && process.platform === "linux" && !process.env.USER) {
|
||||
console.log(common.UBUNTU_SLIM_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isARCRunner()) {
|
||||
console.log(`[!] ${common.ARC_RUNNER_MESSAGE}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const thirdPartyProvider = detectThirdPartyRunnerProvider();
|
||||
|
||||
if (process.env.STATE_selfHosted === "true") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -49,7 +56,11 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
|
|||
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
await handleLinuxCleanup();
|
||||
if (thirdPartyProvider) {
|
||||
await handleAgentBravoCleanup();
|
||||
} else {
|
||||
await handleLinuxCleanup();
|
||||
}
|
||||
break;
|
||||
case "win32":
|
||||
await handleWindowsCleanup();
|
||||
|
|
@ -66,6 +77,38 @@ import { isPlatformSupported, isAgentInstalled } from "./utils";
|
|||
}
|
||||
})();
|
||||
|
||||
async function handleAgentBravoCleanup() {
|
||||
cp.execFileSync("/usr/bin/echo", ["step_policy_jobend"]);
|
||||
|
||||
const doneFile = "/home/agent/done.json";
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
if (!fs.existsSync(doneFile)) {
|
||||
counter++;
|
||||
if (counter > 10) {
|
||||
console.log("timed out");
|
||||
break;
|
||||
}
|
||||
await sleep(1000);
|
||||
} else {
|
||||
console.log(fs.readFileSync(doneFile, "utf-8"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const log = "/home/agent/agent.log";
|
||||
if (fs.existsSync(log)) {
|
||||
console.log("log:");
|
||||
console.log(fs.readFileSync(log, "utf-8"));
|
||||
}
|
||||
|
||||
const status = "/home/agent/agent.status";
|
||||
if (fs.existsSync(status)) {
|
||||
console.log("status:");
|
||||
console.log(fs.readFileSync(status, "utf-8"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLinuxCleanup() {
|
||||
if (process.env.STATE_isTLS === "false" && process.arch === "arm64") {
|
||||
return;
|
||||
|
|
@ -204,6 +247,11 @@ async function handleWindowsCleanup() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (process.arch === "arm64") {
|
||||
console.log(common.ARM64_WINDOWS_RUNNER_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
const p = cp.spawn(
|
||||
"powershell.exe",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -145,3 +145,9 @@ 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.";
|
||||
|
||||
export const UBUNTU_SLIM_MESSAGE =
|
||||
"This job is running on an ubuntu-slim runner. Harden Runner is not supported on ubuntu-slim runners. This job will not be monitored.";
|
||||
|
|
|
|||
|
|
@ -2,4 +2,7 @@ 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";
|
||||
|
|
|
|||
|
|
@ -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 } from "./common";
|
||||
import { ARM64_RUNNER_MESSAGE, ARM64_WINDOWS_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.10/harden-runner_1.7.10_linux_${variant}.tar.gz`,
|
||||
`https://github.com/step-security/agent-ebpf/releases/download/v1.8.6/harden-runner_1.8.6_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.14.3/agent_0.14.3_linux_amd64.tar.gz",
|
||||
"https://github.com/step-security/agent/releases/download/v0.16.0/agent_0.16.0_linux_amd64.tar.gz",
|
||||
undefined,
|
||||
auth
|
||||
);
|
||||
|
|
@ -69,6 +69,60 @@ export async function installAgent(
|
|||
return true;
|
||||
}
|
||||
|
||||
export async function installAgentBravo(configStr: string): Promise<boolean> {
|
||||
// Note: to avoid github rate limiting
|
||||
const token = core.getInput("token", { required: true });
|
||||
const auth = `token ${token}`;
|
||||
|
||||
const variant = process.arch === "x64" ? "amd64" : "arm64";
|
||||
const downloadPath = await tc.downloadTool(
|
||||
`https://github.com/step-security/agent-ebpf/releases/download/v1.8.6/harden-runner-bravo_1.8.6_linux_${variant}.tar.gz`,
|
||||
undefined,
|
||||
auth
|
||||
);
|
||||
|
||||
if (!verifyChecksum(downloadPath, true, variant, "linux", "bravo")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extractPath = await tc.extractTar(downloadPath);
|
||||
cp.execFileSync("cp", [path.join(extractPath, "agent"), "/home/agent/agent"]);
|
||||
|
||||
cp.execSync("chmod +x /home/agent/agent");
|
||||
|
||||
fs.writeFileSync("/home/agent/agent.json", configStr);
|
||||
|
||||
const logStream = fs.openSync("/home/agent/agent.stdout", "a");
|
||||
const agentProcess = cp.spawn("sudo", ["/home/agent/agent"], {
|
||||
cwd: "/home/agent",
|
||||
detached: true,
|
||||
stdio: ["ignore", logStream, logStream],
|
||||
});
|
||||
agentProcess.unref();
|
||||
|
||||
const agentStatus = "/home/agent/agent.status";
|
||||
const deadline = Date.now() + 10000;
|
||||
while (true) {
|
||||
if (!fs.existsSync(agentStatus)) {
|
||||
if (Date.now() >= deadline) {
|
||||
console.log("timed out waiting for bravo agent");
|
||||
if (fs.existsSync("/home/agent/agent.stdout")) {
|
||||
console.log(fs.readFileSync("/home/agent/agent.stdout", "utf-8"));
|
||||
}
|
||||
if (fs.existsSync("/home/agent/agent.log")) {
|
||||
console.log(fs.readFileSync("/home/agent/agent.log", "utf-8"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
} else {
|
||||
console.log(fs.readFileSync(agentStatus, "utf-8"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function installMacosAgent(configStr: string): Promise<boolean> {
|
||||
const token = core.getInput("token", { required: true });
|
||||
const auth = `token ${token}`;
|
||||
|
|
@ -89,7 +143,7 @@ export async function installMacosAgent(configStr: string): Promise<boolean> {
|
|||
|
||||
// Download installer package
|
||||
const downloadUrl =
|
||||
"https://github.com/step-security/agent-releases/releases/download/v0.0.4-mac/macos-installer-0.0.4.tar.gz";
|
||||
"https://github.com/step-security/agent-releases/releases/download/v0.0.5-mac/macos-installer-0.0.5.tar.gz";
|
||||
core.info(`Downloading macOS installer.. : ${downloadUrl}`);
|
||||
const downloadPath = await tc.downloadTool(downloadUrl, undefined, auth);
|
||||
core.info(`✓ Successfully downloaded installer to: ${downloadPath}`);
|
||||
|
|
@ -157,7 +211,7 @@ export async function installWindowsAgent(configStr: string): Promise<boolean> {
|
|||
|
||||
const variant = process.arch === "x64" ? "amd64" : "arm64";
|
||||
if (variant === "arm64") {
|
||||
console.log(ARM64_RUNNER_MESSAGE);
|
||||
console.log(ARM64_WINDOWS_RUNNER_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +226,7 @@ export async function installWindowsAgent(configStr: string): Promise<boolean> {
|
|||
const agentExePath = path.join(agentDir, "agent.exe");
|
||||
|
||||
const downloadPath = await tc.downloadTool(
|
||||
`https://github.com/step-security/agent-releases/releases/download/v1.0.0-win/harden-runner-agent-windows_1.0.0_windows_amd64.tar.gz`,
|
||||
`https://github.com/step-security/agent-releases/releases/download/v1.0.2-win/harden-runner-agent-windows_1.0.2_windows_amd64.tar.gz`,
|
||||
undefined,
|
||||
auth
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export interface Configuration {
|
|||
correlation_id: string;
|
||||
working_directory: string;
|
||||
api_url: string;
|
||||
telemetry_url: string;
|
||||
allowed_endpoints: string;
|
||||
egress_policy: string;
|
||||
disable_telemetry: boolean;
|
||||
|
|
@ -14,6 +15,9 @@ export interface Configuration {
|
|||
private: string;
|
||||
is_debug: boolean;
|
||||
one_time_key: string;
|
||||
api_key: string;
|
||||
use_policy_store: boolean;
|
||||
deploy_on_self_hosted_vm: boolean;
|
||||
}
|
||||
|
||||
export interface PolicyResponse {
|
||||
|
|
@ -25,4 +29,5 @@ export interface PolicyResponse {
|
|||
disable_file_monitoring?: boolean;
|
||||
disable_telemetry?: boolean;
|
||||
egress_policy?: string;
|
||||
policy_name?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ test("merge configs", async () => {
|
|||
correlation_id: "aaaaa",
|
||||
working_directory: "/xyz",
|
||||
api_url: "xyz",
|
||||
telemetry_url: "xyz",
|
||||
allowed_endpoints: "",
|
||||
egress_policy: "audit",
|
||||
disable_telemetry: false,
|
||||
|
|
@ -42,6 +43,9 @@ test("merge configs", async () => {
|
|||
is_github_hosted: true,
|
||||
is_debug: false,
|
||||
one_time_key: "",
|
||||
api_key: "",
|
||||
use_policy_store: false,
|
||||
deploy_on_self_hosted_vm: false,
|
||||
};
|
||||
let policyResponse: PolicyResponse = {
|
||||
owner: "h0x0er",
|
||||
|
|
@ -59,6 +63,7 @@ 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,
|
||||
|
|
@ -69,8 +74,320 @@ test("merge configs", async () => {
|
|||
is_github_hosted: true,
|
||||
is_debug: false,
|
||||
one_time_key: "",
|
||||
api_key: "",
|
||||
use_policy_store: false,
|
||||
deploy_on_self_hosted_vm: 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,
|
||||
deploy_on_self_hosted_vm: 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,
|
||||
deploy_on_self_hosted_vm: 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,
|
||||
deploy_on_self_hosted_vm: 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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,65 @@ 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
|
||||
|
|
|
|||
212
src/setup.ts
212
src/setup.ts
|
|
@ -16,7 +16,7 @@ import {
|
|||
isValidEvent,
|
||||
} from "./cache";
|
||||
import { Configuration, PolicyResponse } from "./interfaces";
|
||||
import { fetchPolicy, mergeConfigs } from "./policy-utils";
|
||||
import { fetchPolicy, fetchPolicyFromStore, 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,15 +25,21 @@ 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_WEB_URL } from "./configs";
|
||||
import {
|
||||
STEPSECURITY_API_URL,
|
||||
STEPSECURITY_TELEMETRY_URL,
|
||||
STEPSECURITY_WEB_URL,
|
||||
} from "./configs";
|
||||
import { isGithubHosted, isTLSEnabled } from "./tls-inspect";
|
||||
import {
|
||||
installAgent,
|
||||
installAgentBravo,
|
||||
installMacosAgent,
|
||||
installWindowsAgent,
|
||||
} from "./install-agent";
|
||||
|
||||
import { chownForFolder, isAgentInstalled, isPlatformSupported } from "./utils";
|
||||
import { chownForFolder, detectThirdPartyRunnerProvider, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted } from "./utils";
|
||||
import { buildBravoConfig } from "./bravo-config";
|
||||
|
||||
interface MonitorResponse {
|
||||
runner_ip_address?: string;
|
||||
|
|
@ -60,6 +66,11 @@ interface MonitorResponse {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isGithubHosted() && process.platform === "linux" && !process.env.USER) {
|
||||
console.log(common.UBUNTU_SLIM_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
var correlation_id = uuidv4();
|
||||
var api_url = STEPSECURITY_API_URL;
|
||||
var web_url = STEPSECURITY_WEB_URL;
|
||||
|
|
@ -70,6 +81,7 @@ 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"),
|
||||
|
|
@ -82,10 +94,55 @@ 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"),
|
||||
deploy_on_self_hosted_vm: core.getBooleanInput("deploy-on-self-hosted-vm"),
|
||||
};
|
||||
|
||||
if (confg.api_key !== "") {
|
||||
core.setSecret(confg.api_key);
|
||||
}
|
||||
|
||||
let policyName = core.getInput("policy");
|
||||
if (policyName !== "") {
|
||||
if (confg.use_policy_store) {
|
||||
console.log(`Fetching policy from policy store`);
|
||||
if (confg.api_key === "") {
|
||||
core.warning(
|
||||
"api-key is not set while use-policy-store is true. Defaulting to audit mode."
|
||||
);
|
||||
confg.egress_policy = "audit";
|
||||
} 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 !== "") {
|
||||
console.log(`Fetching policy from API with name: ${policyName}`);
|
||||
try {
|
||||
let idToken: string = await core.getIDToken();
|
||||
|
|
@ -242,13 +299,46 @@ interface MonitorResponse {
|
|||
const runnerName = process.env.RUNNER_NAME || "";
|
||||
core.info(`RUNNER_NAME: ${runnerName}`);
|
||||
if (!isGithubHosted()) {
|
||||
const thirdPartyProvider = detectThirdPartyRunnerProvider();
|
||||
if (thirdPartyProvider) {
|
||||
const providerLabel = thirdPartyProvider.charAt(0).toUpperCase() + thirdPartyProvider.slice(1);
|
||||
if (process.platform !== "linux") {
|
||||
core.info(`Detected ${providerLabel} runner on ${process.platform}. Bravo agent is Linux-only, skipping install.`);
|
||||
return;
|
||||
}
|
||||
core.info(`Detected ${providerLabel} runner environment. Installing agent-bravo.`);
|
||||
confg.correlation_id = runnerName || confg.correlation_id;
|
||||
await callMonitorEndpoint(api_url, confg);
|
||||
await installAgentForBravo(context.repo.owner, confg);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `selfHosted=true${EOL}`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
core.info(common.SELF_HOSTED_RUNNER_MESSAGE);
|
||||
|
||||
if (confg.egress_policy === "block") {
|
||||
const inContainer = isDocker();
|
||||
const alreadyInstalled = isAgentInstalled(process.platform);
|
||||
|
||||
if (shouldDeployAgentOnSelfHosted(confg.deploy_on_self_hosted_vm, inContainer, alreadyInstalled)) {
|
||||
if (process.platform !== "linux") {
|
||||
core.info("deploy-on-self-hosted-vm is only supported on Linux. Skipping agent deployment.");
|
||||
} else {
|
||||
core.info("deploy-on-self-hosted-vm is enabled. Installing agent on self-hosted runner.");
|
||||
await installAgentForSelfHosted(context.repo.owner, confg);
|
||||
}
|
||||
} else {
|
||||
if (confg.deploy_on_self_hosted_vm && inContainer) {
|
||||
core.info("Skipping agent deployment: running inside a container.");
|
||||
}
|
||||
if (confg.deploy_on_self_hosted_vm && alreadyInstalled) {
|
||||
core.info("Agent already installed on self-hosted runner, skipping installation.");
|
||||
}
|
||||
}
|
||||
|
||||
if (confg.egress_policy === "block" && !confg.deploy_on_self_hosted_vm) {
|
||||
sendAllowedEndpoints(confg.allowed_endpoints);
|
||||
await sleep(5000);
|
||||
}
|
||||
|
|
@ -327,7 +417,8 @@ interface MonitorResponse {
|
|||
return;
|
||||
}
|
||||
|
||||
const configStr = JSON.stringify(confg);
|
||||
const { api_key, use_policy_store, ...agentConfig } = confg;
|
||||
const configStr = JSON.stringify(agentConfig);
|
||||
|
||||
// platform specific
|
||||
let statusFile = "";
|
||||
|
|
@ -402,3 +493,112 @@ export function sleep(ms: number) {
|
|||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function callMonitorEndpoint(api_url: string, confg: Configuration) {
|
||||
const _http = new httpm.HttpClient();
|
||||
_http.requestOptions = { socketTimeout: 3 * 1000 };
|
||||
let statusCode: number | undefined;
|
||||
let addSummary = "false";
|
||||
try {
|
||||
const monitorRequestData = {
|
||||
correlation_id: confg.correlation_id,
|
||||
job: process.env["GITHUB_JOB"],
|
||||
};
|
||||
const resp = await _http.postJson<MonitorResponse>(
|
||||
`${api_url}/github/${process.env["GITHUB_REPOSITORY"]}/actions/runs/${process.env["GITHUB_RUN_ID"]}/monitor`,
|
||||
monitorRequestData
|
||||
);
|
||||
statusCode = resp.statusCode;
|
||||
if (resp.statusCode === 200 && resp.result) {
|
||||
console.log(`Runner IP Address: ${resp.result.runner_ip_address}`);
|
||||
confg.one_time_key = resp.result.one_time_key;
|
||||
addSummary = resp.result.monitoring_started ? "true" : "false";
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`error in connecting to ${api_url}: ${e}`);
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `monitorStatusCode=${statusCode}${EOL}`, { encoding: "utf8" });
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `addSummary=${addSummary}${EOL}`, { encoding: "utf8" });
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, `correlation_id=${confg.correlation_id}${EOL}`, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
export async function installAgentForSelfHosted(owner: string, confg: Configuration) {
|
||||
try {
|
||||
console.log("Installing Harden Runner agent for self-hosted runner");
|
||||
|
||||
let isTLS = await isTLSEnabled(owner);
|
||||
|
||||
if (!isTLS) {
|
||||
console.log("TLS is not enabled for this organization. Agent installation skipped for self-hosted runner.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selfHostedConfig = {
|
||||
customer: owner,
|
||||
working_directory: confg.working_directory,
|
||||
api_url: confg.api_url,
|
||||
api_key: uuidv4(),
|
||||
allowed_endpoints: confg.allowed_endpoints,
|
||||
egress_policy: confg.egress_policy,
|
||||
disable_telemetry: confg.disable_telemetry,
|
||||
disable_sudo: confg.disable_sudo,
|
||||
disable_sudo_and_containers: confg.disable_sudo_and_containers,
|
||||
disable_file_monitoring: confg.disable_file_monitoring,
|
||||
is_github_hosted: false,
|
||||
};
|
||||
const selfHostedConfigStr = JSON.stringify(selfHostedConfig);
|
||||
|
||||
cp.execSync("sudo mkdir -p /home/agent");
|
||||
chownForFolder(process.env.USER, "/home/agent");
|
||||
|
||||
const agentInstalled = await installAgent(isTLS, selfHostedConfigStr);
|
||||
|
||||
if (agentInstalled) {
|
||||
const statusFile = "/home/agent/agent.status";
|
||||
const logFile = "/home/agent/agent.log";
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
if (!fs.existsSync(statusFile)) {
|
||||
counter++;
|
||||
if (counter > 30) {
|
||||
console.log("timed out");
|
||||
if (fs.existsSync(logFile)) {
|
||||
const content = fs.readFileSync(logFile, "utf-8");
|
||||
console.log(content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
await sleep(300);
|
||||
} else {
|
||||
const content = fs.readFileSync(statusFile, "utf-8");
|
||||
console.log(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to install agent for self-hosted runner: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installAgentForBravo(owner: string, confg: Configuration) {
|
||||
try {
|
||||
console.log("Installing Harden Runner bravo agent for third-party runner");
|
||||
|
||||
let isTLS = await isTLSEnabled(owner);
|
||||
|
||||
if (!isTLS) {
|
||||
console.log("TLS is not enabled for this organization. Bravo agent installation skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
const bravoConfigStr = JSON.stringify(buildBravoConfig(confg));
|
||||
|
||||
cp.execSync("sudo mkdir -p /home/agent");
|
||||
chownForFolder(process.env.USER, "/home/agent");
|
||||
|
||||
await installAgentBravo(bravoConfigStr);
|
||||
} catch (error) {
|
||||
console.log(`Failed to install bravo agent: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
src/utils.test.ts
Normal file
158
src/utils.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs, detectThirdPartyRunnerProvider } from "./utils";
|
||||
import * as fs from "fs";
|
||||
|
||||
jest.mock("fs", () => ({
|
||||
...jest.requireActual("fs"),
|
||||
existsSync: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
|
||||
|
||||
describe("shouldDeployAgentOnSelfHosted", () => {
|
||||
test("returns true when deploy flag is true, not container, agent not installed", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(true, false, false)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when deploy flag is false", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(false, false, false)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when running in a container", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(true, true, false)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when agent is already installed", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(true, false, true)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when in container and agent installed", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(true, true, true)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when all conditions are negative", () => {
|
||||
expect(shouldDeployAgentOnSelfHosted(false, true, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAgentInstalled", () => {
|
||||
afterEach(() => {
|
||||
mockedExistsSync.mockReset();
|
||||
});
|
||||
|
||||
test("returns false for linux when status file does not exist", () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
expect(isAgentInstalled("linux")).toBe(false);
|
||||
expect(mockedExistsSync).toHaveBeenCalledWith("/home/agent/agent.status");
|
||||
});
|
||||
|
||||
test("returns true for linux when status file exists", () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
expect(isAgentInstalled("linux")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for win32 when status file does not exist", () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
expect(isAgentInstalled("win32")).toBe(false);
|
||||
expect(mockedExistsSync).toHaveBeenCalledWith("C:\\agent\\agent.status");
|
||||
});
|
||||
|
||||
test("returns false for darwin when status file does not exist", () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
expect(isAgentInstalled("darwin")).toBe(false);
|
||||
expect(mockedExistsSync).toHaveBeenCalledWith("/opt/step-security/agent.status");
|
||||
});
|
||||
|
||||
test("returns false for unsupported platform", () => {
|
||||
expect(isAgentInstalled("freebsd" as NodeJS.Platform)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPlatformSupported", () => {
|
||||
test("returns true for linux", () => {
|
||||
expect(isPlatformSupported("linux")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for win32", () => {
|
||||
expect(isPlatformSupported("win32")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for darwin", () => {
|
||||
expect(isPlatformSupported("darwin")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for unsupported platform", () => {
|
||||
expect(isPlatformSupported("freebsd" as NodeJS.Platform)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAnnotationLogs", () => {
|
||||
test("throws for unsupported platform", () => {
|
||||
expect(() => getAnnotationLogs("freebsd" as NodeJS.Platform)).toThrow("platform not supported");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectThirdPartyRunnerProvider", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.DEPOT_RUNNER;
|
||||
delete process.env.NAMESPACE_GITHUB_RUNTIME;
|
||||
delete process.env.RUNNER_NAME;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test("returns depot when DEPOT_RUNNER=1", () => {
|
||||
process.env.DEPOT_RUNNER = "1";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("depot");
|
||||
});
|
||||
|
||||
test("returns null when DEPOT_RUNNER=0", () => {
|
||||
process.env.DEPOT_RUNNER = "0";
|
||||
expect(detectThirdPartyRunnerProvider()).toBeNull();
|
||||
});
|
||||
|
||||
test("returns namespace when NAMESPACE_GITHUB_RUNTIME is set", () => {
|
||||
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("namespace");
|
||||
});
|
||||
|
||||
test("returns warp for RUNNER_NAME prefix warp-", () => {
|
||||
process.env.RUNNER_NAME = "warp-4x-x64-abc";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("warp");
|
||||
});
|
||||
|
||||
test("returns blacksmith for RUNNER_NAME prefix blacksmith-", () => {
|
||||
process.env.RUNNER_NAME = "blacksmith-01kpj-4vcpu";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("blacksmith");
|
||||
});
|
||||
|
||||
test("returns null when no env vars match", () => {
|
||||
expect(detectThirdPartyRunnerProvider()).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for a non-matching RUNNER_NAME", () => {
|
||||
process.env.RUNNER_NAME = "GitHub Actions 1";
|
||||
expect(detectThirdPartyRunnerProvider()).toBeNull();
|
||||
});
|
||||
|
||||
test("depot takes precedence over namespace", () => {
|
||||
process.env.DEPOT_RUNNER = "1";
|
||||
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("depot");
|
||||
});
|
||||
|
||||
test("namespace takes precedence over warp runner name prefix", () => {
|
||||
process.env.NAMESPACE_GITHUB_RUNTIME = "something";
|
||||
process.env.RUNNER_NAME = "warp-x";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("namespace");
|
||||
});
|
||||
|
||||
test("warp takes precedence over blacksmith when both prefixes seen (warp wins on name check order)", () => {
|
||||
process.env.RUNNER_NAME = "warp-x";
|
||||
expect(detectThirdPartyRunnerProvider()).toBe("warp");
|
||||
});
|
||||
});
|
||||
19
src/utils.ts
19
src/utils.ts
|
|
@ -32,6 +32,25 @@ export function isAgentInstalled(platform: NodeJS.Platform) {
|
|||
}
|
||||
}
|
||||
|
||||
export function shouldDeployAgentOnSelfHosted(
|
||||
deployOnSelfHostedVm: boolean,
|
||||
isContainer: boolean,
|
||||
agentAlreadyInstalled: boolean
|
||||
): boolean {
|
||||
return deployOnSelfHostedVm && !isContainer && !agentAlreadyInstalled;
|
||||
}
|
||||
|
||||
export type ThirdPartyRunnerProvider = "depot" | "namespace" | "warp" | "blacksmith";
|
||||
|
||||
export function detectThirdPartyRunnerProvider(): ThirdPartyRunnerProvider | null {
|
||||
if (process.env["DEPOT_RUNNER"] === "1") return "depot";
|
||||
if (process.env["NAMESPACE_GITHUB_RUNTIME"]) return "namespace";
|
||||
const runnerName = process.env["RUNNER_NAME"] ?? "";
|
||||
if (runnerName.startsWith("warp-")) return "warp";
|
||||
if (runnerName.startsWith("blacksmith-")) return "blacksmith";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAnnotationLogs(platform: NodeJS.Platform) {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue