Skip to content

Commit 40b99cf

Browse files
Merge pull request #682 from rohan-stepsecurity/rp/feat/codebuild-self-v2
feat: avoid sudo when running as root
2 parents 051ec05 + fedec02 commit 40b99cf

10 files changed

Lines changed: 166 additions & 38 deletions

File tree

dist/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31910,14 +31910,28 @@ function getRunnerUser() {
3191031910
return undefined;
3191131911
}
3191231912
}
31913-
function chownForFolder(newOwner, target) {
31913+
function getPrivilegeMode() {
31914+
try {
31915+
if (os.userInfo().uid === 0) {
31916+
return "root";
31917+
}
31918+
}
31919+
catch (_a) {
31920+
// fall through to sudo
31921+
}
31922+
return "sudo";
31923+
}
31924+
function chownForFolder(newOwner, target, useDirectPrivileges = false) {
3191431925
if (!newOwner) {
3191531926
console.log(`Unable to determine runner user; skipping chown of ${target}`);
3191631927
return;
3191731928
}
31918-
let cmd = "sudo";
31919-
let args = ["chown", "-R", newOwner, target];
31920-
cp.execFileSync(cmd, args);
31929+
if (useDirectPrivileges) {
31930+
cp.execFileSync("chown", ["-R", newOwner, target]);
31931+
}
31932+
else {
31933+
cp.execFileSync("sudo", ["chown", "-R", newOwner, target]);
31934+
}
3192131935
}
3192231936
function isAgentInstalled(platform) {
3192331937
switch (platform) {

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/post/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31916,14 +31916,28 @@ function getRunnerUser() {
3191631916
return undefined;
3191731917
}
3191831918
}
31919-
function chownForFolder(newOwner, target) {
31919+
function getPrivilegeMode() {
31920+
try {
31921+
if (os.userInfo().uid === 0) {
31922+
return "root";
31923+
}
31924+
}
31925+
catch (_a) {
31926+
// fall through to sudo
31927+
}
31928+
return "sudo";
31929+
}
31930+
function chownForFolder(newOwner, target, useDirectPrivileges = false) {
3192031931
if (!newOwner) {
3192131932
console.log(`Unable to determine runner user; skipping chown of ${target}`);
3192231933
return;
3192331934
}
31924-
let cmd = "sudo";
31925-
let args = ["chown", "-R", newOwner, target];
31926-
cp.execFileSync(cmd, args);
31935+
if (useDirectPrivileges) {
31936+
cp.execFileSync("chown", ["-R", newOwner, target]);
31937+
}
31938+
else {
31939+
cp.execFileSync("sudo", ["chown", "-R", newOwner, target]);
31940+
}
3192731941
}
3192831942
function isAgentInstalled(platform) {
3192931943
switch (platform) {

dist/post/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pre/index.js

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85108,14 +85108,28 @@ function getRunnerUser() {
8510885108
return undefined;
8510985109
}
8511085110
}
85111-
function chownForFolder(newOwner, target) {
85111+
function getPrivilegeMode() {
85112+
try {
85113+
if (external_os_.userInfo().uid === 0) {
85114+
return "root";
85115+
}
85116+
}
85117+
catch (_a) {
85118+
// fall through to sudo
85119+
}
85120+
return "sudo";
85121+
}
85122+
function chownForFolder(newOwner, target, useDirectPrivileges = false) {
8511285123
if (!newOwner) {
8511385124
console.log(`Unable to determine runner user; skipping chown of ${target}`);
8511485125
return;
8511585126
}
85116-
let cmd = "sudo";
85117-
let args = ["chown", "-R", newOwner, target];
85118-
external_child_process_.execFileSync(cmd, args);
85127+
if (useDirectPrivileges) {
85128+
external_child_process_.execFileSync("chown", ["-R", newOwner, target]);
85129+
}
85130+
else {
85131+
external_child_process_.execFileSync("sudo", ["chown", "-R", newOwner, target]);
85132+
}
8511985133
}
8512085134
function isAgentInstalled(platform) {
8512185135
switch (platform) {
@@ -85678,8 +85692,8 @@ function installAgent(isTLS, configStr) {
8567885692
return true;
8567985693
});
8568085694
}
85681-
function installAgentBravo(configStr) {
85682-
return install_agent_awaiter(this, void 0, void 0, function* () {
85695+
function installAgentBravo(configStr_1) {
85696+
return install_agent_awaiter(this, arguments, void 0, function* (configStr, useDirectPrivileges = false) {
8568385697
// Note: to avoid github rate limiting
8568485698
const token = lib_core.getInput("token", { required: true });
8568585699
const auth = `token ${token}`;
@@ -85693,11 +85707,14 @@ function installAgentBravo(configStr) {
8569385707
external_child_process_.execSync("chmod +x /home/agent/agent");
8569485708
external_fs_.writeFileSync("/home/agent/agent.json", configStr);
8569585709
const logStream = external_fs_.openSync("/home/agent/agent.stdout", "a");
85696-
const agentProcess = external_child_process_.spawn("sudo", ["/home/agent/agent"], {
85710+
const spawnOptions = {
8569785711
cwd: "/home/agent",
8569885712
detached: true,
8569985713
stdio: ["ignore", logStream, logStream],
85700-
});
85714+
};
85715+
const agentProcess = useDirectPrivileges
85716+
? external_child_process_.spawn("/home/agent/agent", [], spawnOptions)
85717+
: external_child_process_.spawn("sudo", ["/home/agent/agent"], spawnOptions);
8570185718
agentProcess.unref();
8570285719
const agentStatus = "/home/agent/agent.status";
8570385720
const deadline = Date.now() + 10000;
@@ -86140,7 +86157,7 @@ process.on("unhandledRejection", (reason) => {
8614086157
return;
8614186158
}
8614286159
case "linux":
86143-
yield installAgentForBravo(github.context.repo.owner, bravoConfigStr);
86160+
yield installAgentForBravo(github.context.repo.owner, bravoConfigStr, thirdPartyProvider);
8614486161
return;
8614586162
}
8614686163
}
@@ -86384,7 +86401,7 @@ function installAgentForSelfHosted(owner, confg) {
8638486401
}
8638586402
});
8638686403
}
86387-
function installAgentForBravo(owner, bravoConfigStr) {
86404+
function installAgentForBravo(owner, bravoConfigStr, provider) {
8638886405
return setup_awaiter(this, void 0, void 0, function* () {
8638986406
try {
8639086407
console.log("Installing Harden Runner bravo agent for third-party runner");
@@ -86393,9 +86410,17 @@ function installAgentForBravo(owner, bravoConfigStr) {
8639386410
console.log("TLS is not enabled for this organization. Bravo agent installation skipped.");
8639486411
return;
8639586412
}
86396-
external_child_process_.execSync("sudo mkdir -p /home/agent");
86397-
chownForFolder(getRunnerUser(), "/home/agent");
86398-
yield installAgentBravo(bravoConfigStr);
86413+
const privilegeMode = getPrivilegeMode();
86414+
if (isDocker() && privilegeMode !== "root") {
86415+
console.log("Running inside a container without root privileges. Bravo agent installation skipped.");
86416+
return;
86417+
}
86418+
// CodeBuild containers run as root without a sudo binary; other
86419+
// providers keep the existing sudo-based install.
86420+
const useDirectPrivileges = provider === "codebuild" && privilegeMode === "root";
86421+
external_child_process_.execSync(useDirectPrivileges ? "mkdir -p /home/agent" : "sudo mkdir -p /home/agent");
86422+
chownForFolder(getRunnerUser(), "/home/agent", useDirectPrivileges);
86423+
yield installAgentBravo(bravoConfigStr, useDirectPrivileges);
8639986424
}
8640086425
catch (error) {
8640186426
console.log(`Failed to install bravo agent: ${error.message}`);

dist/pre/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/install-agent.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ export async function installAgent(
6969
return true;
7070
}
7171

72-
export async function installAgentBravo(configStr: string): Promise<boolean> {
72+
export async function installAgentBravo(
73+
configStr: string,
74+
useDirectPrivileges: boolean = false
75+
): Promise<boolean> {
7376
// Note: to avoid github rate limiting
7477
const token = core.getInput("token", { required: true });
7578
const auth = `token ${token}`;
@@ -93,11 +96,14 @@ export async function installAgentBravo(configStr: string): Promise<boolean> {
9396
fs.writeFileSync("/home/agent/agent.json", configStr);
9497

9598
const logStream = fs.openSync("/home/agent/agent.stdout", "a");
96-
const agentProcess = cp.spawn("sudo", ["/home/agent/agent"], {
99+
const spawnOptions: cp.SpawnOptions = {
97100
cwd: "/home/agent",
98101
detached: true,
99102
stdio: ["ignore", logStream, logStream],
100-
});
103+
};
104+
const agentProcess = useDirectPrivileges
105+
? cp.spawn("/home/agent/agent", [], spawnOptions)
106+
: cp.spawn("sudo", ["/home/agent/agent"], spawnOptions);
101107
agentProcess.unref();
102108

103109
const agentStatus = "/home/agent/agent.status";

src/setup.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
installWindowsAgent,
3838
} from "./install-agent";
3939

40-
import { chownForFolder, getRunnerUser, detectThirdPartyRunnerProvider, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted } from "./utils";
40+
import { chownForFolder, getRunnerUser, getPrivilegeMode, detectThirdPartyRunnerProvider, isAgentInstalled, isPlatformSupported, shouldDeployAgentOnSelfHosted, ThirdPartyRunnerProvider } from "./utils";
4141
import { buildBravoConfig } from "./bravo-config";
4242

4343
interface MonitorResponse {
@@ -340,7 +340,7 @@ process.on("unhandledRejection", (reason) => {
340340
return;
341341
}
342342
case "linux":
343-
await installAgentForBravo(context.repo.owner, bravoConfigStr);
343+
await installAgentForBravo(context.repo.owner, bravoConfigStr, thirdPartyProvider);
344344
return;
345345
}
346346
}
@@ -619,7 +619,11 @@ export async function installAgentForSelfHosted(owner: string, confg: Configurat
619619
}
620620
}
621621

622-
export async function installAgentForBravo(owner: string, bravoConfigStr: string) {
622+
export async function installAgentForBravo(
623+
owner: string,
624+
bravoConfigStr: string,
625+
provider: ThirdPartyRunnerProvider
626+
) {
623627
try {
624628
console.log("Installing Harden Runner bravo agent for third-party runner");
625629

@@ -630,10 +634,23 @@ export async function installAgentForBravo(owner: string, bravoConfigStr: string
630634
return;
631635
}
632636

633-
cp.execSync("sudo mkdir -p /home/agent");
634-
chownForFolder(getRunnerUser(), "/home/agent");
637+
const privilegeMode = getPrivilegeMode();
638+
639+
if (isDocker() && privilegeMode !== "root") {
640+
console.log(
641+
"Running inside a container without root privileges. Bravo agent installation skipped."
642+
);
643+
return;
644+
}
645+
646+
// CodeBuild containers run as root without a sudo binary; other
647+
// providers keep the existing sudo-based install.
648+
const useDirectPrivileges = provider === "codebuild" && privilegeMode === "root";
649+
650+
cp.execSync(useDirectPrivileges ? "mkdir -p /home/agent" : "sudo mkdir -p /home/agent");
651+
chownForFolder(getRunnerUser(), "/home/agent", useDirectPrivileges);
635652

636-
await installAgentBravo(bravoConfigStr);
653+
await installAgentBravo(bravoConfigStr, useDirectPrivileges);
637654
} catch (error) {
638655
console.log(`Failed to install bravo agent: ${error.message}`);
639656
}

src/utils.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs, detectThirdPartyRunnerProvider } from "./utils";
1+
import { shouldDeployAgentOnSelfHosted, isAgentInstalled, isPlatformSupported, getAnnotationLogs, detectThirdPartyRunnerProvider, getPrivilegeMode } from "./utils";
22
import * as fs from "fs";
3+
import * as os from "os";
34

45
jest.mock("fs", () => ({
56
...jest.requireActual("fs"),
67
existsSync: jest.fn(),
78
}));
89

10+
jest.mock("os", () => ({
11+
...jest.requireActual("os"),
12+
userInfo: jest.fn(),
13+
}));
14+
915
const mockedExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
16+
const mockedUserInfo = os.userInfo as jest.MockedFunction<typeof os.userInfo>;
1017

1118
describe("shouldDeployAgentOnSelfHosted", () => {
1219
test("returns true when deploy flag is true, not container, agent not installed", () => {
@@ -91,6 +98,29 @@ describe("getAnnotationLogs", () => {
9198
});
9299
});
93100

101+
describe("getPrivilegeMode", () => {
102+
afterEach(() => {
103+
mockedUserInfo.mockReset();
104+
});
105+
106+
test("returns root when uid is 0", () => {
107+
mockedUserInfo.mockReturnValue({ uid: 0 } as os.UserInfo<string>);
108+
expect(getPrivilegeMode()).toBe("root");
109+
});
110+
111+
test("returns sudo when uid is non-zero", () => {
112+
mockedUserInfo.mockReturnValue({ uid: 1000 } as os.UserInfo<string>);
113+
expect(getPrivilegeMode()).toBe("sudo");
114+
});
115+
116+
test("returns sudo when userInfo throws", () => {
117+
mockedUserInfo.mockImplementation(() => {
118+
throw new Error("no user info");
119+
});
120+
expect(getPrivilegeMode()).toBe("sudo");
121+
});
122+
});
123+
94124
describe("detectThirdPartyRunnerProvider", () => {
95125
const originalEnv = process.env;
96126

src/utils.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,36 @@ export function getRunnerUser(): string | undefined {
2828
}
2929
}
3030

31-
export function chownForFolder(newOwner: string | undefined, target: string) {
31+
// How the current process should obtain root privileges. Some runner
32+
// environments (e.g. AWS CodeBuild containers) run as root without the
33+
// sudo binary installed.
34+
export type PrivilegeMode = "root" | "sudo";
35+
36+
export function getPrivilegeMode(): PrivilegeMode {
37+
try {
38+
if (os.userInfo().uid === 0) {
39+
return "root";
40+
}
41+
} catch {
42+
// fall through to sudo
43+
}
44+
return "sudo";
45+
}
46+
47+
export function chownForFolder(
48+
newOwner: string | undefined,
49+
target: string,
50+
useDirectPrivileges: boolean = false
51+
) {
3252
if (!newOwner) {
3353
console.log(`Unable to determine runner user; skipping chown of ${target}`);
3454
return;
3555
}
36-
let cmd = "sudo";
37-
let args = ["chown", "-R", newOwner, target];
38-
cp.execFileSync(cmd, args);
56+
if (useDirectPrivileges) {
57+
cp.execFileSync("chown", ["-R", newOwner, target]);
58+
} else {
59+
cp.execFileSync("sudo", ["chown", "-R", newOwner, target]);
60+
}
3961
}
4062

4163
export function isAgentInstalled(platform: NodeJS.Platform) {

0 commit comments

Comments
 (0)