Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions report/src/lib/build-example.js → core/lib/build-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ const ruleTypeToParam = {
* @param {Array<{host: string, port: string, ruleType: string}>} auditedRows
* @param {string} actionRepo
* @param {string} actionRef - the ref (tag or commit SHA) this action was invoked with
* @param {{actionName?: string, runCommand?: string}} [options] - actionName
* selects which sub-action the example step uses ("setup" for the
* two-step setup+report flow, "run" for the self-contained run action);
* runCommand, only meaningful for actionName "run", is the original
* `run:` input to preserve in the example step.
* @returns {string}
*/
export function buildRestrictExample(auditedRows, actionRepo, actionRef) {
export function buildRestrictExample(auditedRows, actionRepo, actionRef, { actionName = "setup", runCommand } = {}) {
if (!auditedRows || auditedRows.length === 0) return "";

// A 40-char SHA is opaque to the reader and specific to this run, so show a
Expand All @@ -34,8 +39,17 @@ export function buildRestrictExample(auditedRows, actionRepo, actionRef) {
// Build YAML lines
let yaml = "";
yaml += "- name: Start Buildcage in restrict mode\n";
yaml += ` uses: ${actionRepo}/setup@${ref}\n`;
yaml += ` uses: ${actionRepo}/${actionName}@${ref}\n`;
yaml += " with:\n";
// The `run` action is a single self-contained step, so its example must
// repeat the `run:` command alongside the mode switch to stay copy-pasteable
// (matches `run:`'s position as the first input in run/action.yml).
if (actionName === "run" && runCommand) {
yaml += " run: |\n";
for (const line of runCommand.split(/\r?\n/)) {
yaml += ` ${line}\n`;
}
}
yaml += " proxy_mode: restrict\n";
for (const [param, rules] of groups) {
yaml += ` ${param}: >-\n`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,66 @@ describe("buildRestrictExample", () => {
)
);
});

it("uses the run action and includes the run command when actionName is 'run'", () => {
const rows = [
{ host: "registry.npmjs.org", port: "443", ruleType: "HTTPS", count: 5 },
];
assert.equal(
buildRestrictExample(rows, REPO, REF, { actionName: "run", runCommand: "npm install" }),
wrap(
[
"- name: Start Buildcage in restrict mode",
` uses: ${REPO}/run@${REF}`,
" with:",
" run: |",
" npm install",
" proxy_mode: restrict",
" allowed_https_rules: >-",
" registry.npmjs.org:443",
].join("\n") + "\n",
)
);
});

it("preserves multi-line run commands, indented under run: |", () => {
const rows = [
{ host: "registry.npmjs.org", port: "443", ruleType: "HTTPS", count: 1 },
];
assert.equal(
buildRestrictExample(rows, REPO, REF, { actionName: "run", runCommand: "npm ci\nnpm test" }),
wrap(
[
"- name: Start Buildcage in restrict mode",
` uses: ${REPO}/run@${REF}`,
" with:",
" run: |",
" npm ci",
" npm test",
" proxy_mode: restrict",
" allowed_https_rules: >-",
" registry.npmjs.org:443",
].join("\n") + "\n",
)
);
});

it("actionName 'run' without a runCommand omits the run: block", () => {
const rows = [
{ host: "registry.npmjs.org", port: "443", ruleType: "HTTPS", count: 1 },
];
assert.equal(
buildRestrictExample(rows, REPO, REF, { actionName: "run" }),
wrap(
[
"- name: Start Buildcage in restrict mode",
` uses: ${REPO}/run@${REF}`,
" with:",
" proxy_mode: restrict",
" allowed_https_rules: >-",
" registry.npmjs.org:443",
].join("\n") + "\n",
)
);
});
});
5 changes: 5 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ safe when those steps run truly concurrently via GitHub Actions' `background`/`w
namespaced by the same per-step random suffix, so concurrent steps never recreate or tear down
each other's containers.

In `audit` mode, the Job Summary also includes a ready-to-paste `restrict` mode example — a
`run` step with `proxy_mode: restrict` and allowlist rules generated from the hosts observed
during the audited run — mirroring the same example the `report` action generates for
`setup`/`report` workflows.

### Parameters

| Parameter | Required | Default | Description |
Expand Down
10 changes: 7 additions & 3 deletions report/dist/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ let markdown = `## Outbound Traffic Report during Docker Build (${report.mode} m
if (isAudit) {
const audited = isExplicit ? aggregateAllowedHosts(builds, "AUDIT") : report.sections.audited || [];
audited.length > 0 && (markdown += "### 📋 Audited Hosts\n\n" + markdownTable(audited) + "\n"),
markdown += function(auditedRows, actionRepo, actionRef) {
markdown += function(auditedRows, actionRepo, actionRef, {actionName: actionName = "setup", runCommand: runCommand} = {}) {
if (!auditedRows || 0 === auditedRows.length) return "";
const ref = /^[0-9a-f]{40}$/i.test(actionRef) ? "<sha>" : actionRef, groups = new Map;
for (const r of auditedRows) {
Expand All @@ -230,8 +230,12 @@ if (isAudit) {
}
if (0 === groups.size) return "";
let yaml = "";
yaml += "- name: Start Buildcage in restrict mode\n", yaml += ` uses: ${actionRepo}/setup@${ref}\n`,
yaml += " with:\n", yaml += " proxy_mode: restrict\n";
if (yaml += "- name: Start Buildcage in restrict mode\n", yaml += ` uses: ${actionRepo}/${actionName}@${ref}\n`,
yaml += " with:\n", "run" === actionName && runCommand) {
yaml += " run: |\n";
for (const line of runCommand.split(/\r?\n/)) yaml += ` ${line}\n`;
}
yaml += " proxy_mode: restrict\n";
for (const [param, rules] of groups) {
yaml += ` ${param}: >-\n`;
for (const rule of rules) yaml += ` ${rule}\n`;
Expand Down
2 changes: 1 addition & 1 deletion report/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { buildRestrictExample } from "./lib/build-example.js";
import { buildRestrictExample } from "../../core/lib/build-example.js";
import { renderCommunicationDetails } from "./lib/command-log.js";
import { selectAllRefs, parseVertexAllowedLog, aggregateAllowedHosts } from "./lib/vertex-log.js";
import { createAnnotation } from "../../core/lib/annotation.js";
Expand Down
86 changes: 63 additions & 23 deletions run/dist/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7295,6 +7295,12 @@ function runIsolated({scriptPath: scriptPath, proxyPid: proxyPid, workdir: workd
}
}

const ruleTypeToParam = {
HTTPS: "allowed_https_rules",
HTTP: "allowed_http_rules",
IP: "allowed_ip_rules"
};

function markdownTable(rows, {showReason: showReason = !1} = {}) {
if (showReason) {
const lines = [ "| Host | Rule | Reason | Count |", "| --- | --- | --- | ---: |" ];
Expand All @@ -7306,29 +7312,60 @@ function markdownTable(rows, {showReason: showReason = !1} = {}) {
return lines.join("\n");
}

function writeReport(report, {stepLabel: stepLabel, failOnBlocked: failOnBlocked} = {}) {
const markdown = function(report, {stepLabel: stepLabel} = {}) {
if (null === report.mode) return `### 🧰 Run${stepLabel ? ` — ${stepLabel}` : ""}\n\nNo proxy logs found.\n`;
const isAudit = "audit" === report.mode;
let markdown = `### 🧰 Run${stepLabel ? ` — ${stepLabel}` : ""} (${report.mode} mode)\n\n`;
if (isAudit) {
const audited = report.sections.audited || [];
audited.length > 0 && (markdown += "**📋 Audited Hosts**\n\n" + markdownTable(audited) + "\n\n");
const blocked = report.sections.blocked || [];
blocked.length > 0 && (markdown += "**🚫 Blocked Hosts**\n\n" + markdownTable(blocked, {
showReason: !0
}) + "\n\n");
} else {
const allowed = report.sections.allowed || [];
allowed.length > 0 && (markdown += "**✅ Allowed Hosts**\n\n" + markdownTable(allowed) + "\n\n");
const blocked = report.sections.blocked || [];
blocked.length > 0 && (markdown += "**🚫 Blocked Hosts**\n\n" + markdownTable(blocked, {
showReason: !0
}) + "\n\n");
}
return markdown;
}(report, {
stepLabel: stepLabel
function buildReportMarkdown(report, {stepLabel: stepLabel, actionRepo: actionRepo, actionRef: actionRef, runCommand: runCommand} = {}) {
if (null === report.mode) return `### 🧰 Run${stepLabel ? ` — ${stepLabel}` : ""}\n\nNo proxy logs found.\n`;
const isAudit = "audit" === report.mode;
let markdown = `### 🧰 Run${stepLabel ? ` — ${stepLabel}` : ""} (${report.mode} mode)\n\n`;
if (isAudit) {
const audited = report.sections.audited || [];
audited.length > 0 && (markdown += "**📋 Audited Hosts**\n\n" + markdownTable(audited) + "\n\n"),
markdown += function(auditedRows, actionRepo, actionRef, {actionName: actionName = "setup", runCommand: runCommand} = {}) {
if (!auditedRows || 0 === auditedRows.length) return "";
const ref = /^[0-9a-f]{40}$/i.test(actionRef) ? "<sha>" : actionRef, groups = new Map;
for (const r of auditedRows) {
const param = ruleTypeToParam[r.ruleType];
param && (groups.has(param) || groups.set(param, []), groups.get(param).push(`${r.host}:${r.port}`));
}
if (0 === groups.size) return "";
let yaml = "";
if (yaml += "- name: Start Buildcage in restrict mode\n", yaml += ` uses: ${actionRepo}/${actionName}@${ref}\n`,
yaml += " with:\n", "run" === actionName && runCommand) {
yaml += " run: |\n";
for (const line of runCommand.split(/\r?\n/)) yaml += ` ${line}\n`;
}
yaml += " proxy_mode: restrict\n";
for (const [param, rules] of groups) {
yaml += ` ${param}: >-\n`;
for (const rule of rules) yaml += ` ${rule}\n`;
}
let md = "\n<details>\n";
return md += "<summary>🛡️ Switch to restrict mode</summary>\n\n", md += "```yaml\n",
md += yaml, md += "```\n\n", md += "</details>\n", md;
}(audited, actionRepo, actionRef, {
actionName: "run",
runCommand: runCommand
});
const blocked = report.sections.blocked || [];
blocked.length > 0 && (markdown += "**🚫 Blocked Hosts**\n\n" + markdownTable(blocked, {
showReason: !0
}) + "\n\n");
} else {
const allowed = report.sections.allowed || [];
allowed.length > 0 && (markdown += "**✅ Allowed Hosts**\n\n" + markdownTable(allowed) + "\n\n");
const blocked = report.sections.blocked || [];
blocked.length > 0 && (markdown += "**🚫 Blocked Hosts**\n\n" + markdownTable(blocked, {
showReason: !0
}) + "\n\n");
}
return markdown;
}

function writeReport(report, {stepLabel: stepLabel, failOnBlocked: failOnBlocked, actionRepo: actionRepo, actionRef: actionRef, runCommand: runCommand} = {}) {
const markdown = buildReportMarkdown(report, {
stepLabel: stepLabel,
actionRepo: actionRepo,
actionRef: actionRef,
runCommand: runCommand
}), summaryFile = process.env.GITHUB_STEP_SUMMARY;
summaryFile ? node_fs.appendFileSync(summaryFile, markdown) : console.log(markdown);
const debugSummaryFile = process.env.BUILDCAGE_RUN_DEBUG_SUMMARY_FILE;
Expand Down Expand Up @@ -7482,6 +7519,9 @@ process.argv[1] === node_url.fileURLToPath("undefined" == typeof document ? requ
return JSON.parse(jsonOutput);
}(containerName);
writeReport(report, {
actionRepo: actionRepo,
actionRef: actionRef,
runCommand: runInput,
failOnBlocked: "true" === (env.INPUT_FAIL_ON_BLOCKED || "true").toLowerCase()
});
} catch (e) {
Expand Down
8 changes: 5 additions & 3 deletions run/src/lib/report.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { createAnnotation } from "../../../core/lib/annotation.js";
import { buildRestrictExample } from "../../../core/lib/build-example.js";

/**
* Fetch the structured HAProxy-log report from the (still-running) proxy
Expand Down Expand Up @@ -33,7 +34,7 @@ function markdownTable(rows, { showReason = false } = {}) {
* report. Each `run` step gets its own section (rather than one report
* per job), matching the "one proxy container per step" execution model.
*/
export function buildReportMarkdown(report, { stepLabel } = {}) {
export function buildReportMarkdown(report, { stepLabel, actionRepo, actionRef, runCommand } = {}) {
if (report.mode === null) {
return `### 🧰 Run${stepLabel ? ` — ${stepLabel}` : ""}\n\nNo proxy logs found.\n`;
}
Expand All @@ -44,6 +45,7 @@ export function buildReportMarkdown(report, { stepLabel } = {}) {
if (isAudit) {
const audited = report.sections.audited || [];
if (audited.length > 0) markdown += "**📋 Audited Hosts**\n\n" + markdownTable(audited) + "\n\n";
markdown += buildRestrictExample(audited, actionRepo, actionRef, { actionName: "run", runCommand });
const blocked = report.sections.blocked || [];
if (blocked.length > 0) markdown += "**🚫 Blocked Hosts**\n\n" + markdownTable(blocked, { showReason: true }) + "\n\n";
} else {
Expand All @@ -61,8 +63,8 @@ export function buildReportMarkdown(report, { stepLabel } = {}) {
* set the exit code for blocked connections, mirroring report/src/main.js's
* behavior but scoped to a single run step's proxy container.
*/
export function writeReport(report, { stepLabel, failOnBlocked } = {}) {
const markdown = buildReportMarkdown(report, { stepLabel });
export function writeReport(report, { stepLabel, failOnBlocked, actionRepo, actionRef, runCommand } = {}) {
const markdown = buildReportMarkdown(report, { stepLabel, actionRepo, actionRef, runCommand });
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
if (summaryFile) {
appendFileSync(summaryFile, markdown);
Expand Down
39 changes: 38 additions & 1 deletion run/src/lib/report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import assert from "node:assert/strict";
import { writeFileSync } from "node:fs";
import { join } from "node:path";

import { writeReport } from "./report.js";
import { writeReport, buildReportMarkdown } from "./report.js";
import { withScratchDir } from "./isolated-exec.js";

// writeReport reads GITHUB_STEP_SUMMARY/BUILDCAGE_RUN_DEBUG_SUMMARY_FILE
Expand Down Expand Up @@ -66,3 +66,40 @@ describe("writeReport", () => {
assert.equal(writeReportWithSummary(report, { failOnBlocked: true }), undefined);
});
});

describe("buildReportMarkdown", () => {
it("includes a restrict-mode example (as a `run` step) in audit mode with audited hosts", () => {
const report = {
mode: "audit",
blockedCount: 0,
sections: {
audited: [{ host: "registry.npmjs.org", port: "443", ruleType: "HTTPS", count: 3 }],
},
};
const markdown = buildReportMarkdown(report, {
actionRepo: "dash14/buildcage",
actionRef: "v2",
runCommand: "npm install",
});
assert.match(markdown, /Switch to restrict mode/);
assert.match(markdown, /uses: dash14\/buildcage\/run@v2/);
assert.match(markdown, /run: \|\n\s+npm install/);
assert.match(markdown, /allowed_https_rules: >-\n\s+registry\.npmjs\.org:443/);
});

it("omits the restrict-mode example in restrict mode", () => {
const report = {
mode: "restrict",
blockedCount: 0,
sections: { allowed: [{ host: "registry.npmjs.org", port: "443", ruleType: "HTTPS", count: 3 }] },
};
const markdown = buildReportMarkdown(report, { actionRepo: "dash14/buildcage", actionRef: "v2" });
assert.doesNotMatch(markdown, /Switch to restrict mode/);
});

it("omits the restrict-mode example in audit mode with no audited hosts", () => {
const report = { mode: "audit", blockedCount: 0, sections: {} };
const markdown = buildReportMarkdown(report, { actionRepo: "dash14/buildcage", actionRef: "v2" });
assert.doesNotMatch(markdown, /Switch to restrict mode/);
});
});
3 changes: 3 additions & 0 deletions run/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ async function main() {
try {
const report = fetchReport(containerName);
writeReport(report, {
actionRepo,
actionRef,
runCommand: runInput,
failOnBlocked: (env.INPUT_FAIL_ON_BLOCKED || "true").toLowerCase() === "true",
});
} catch (e) {
Expand Down
Loading