fix(e2e): fail closed on readable previews

This commit is contained in:
Dotta 2026-09-04 13:34:21 -05:00
parent e7a073778e
commit e5857b602e
5 changed files with 37 additions and 11 deletions

View File

@ -1108,11 +1108,12 @@ jobs:
if: always()
run: |
set -euo pipefail
if ! command -v convert >/dev/null 2>&1; then
if ! command -v convert >/dev/null 2>&1 || ! command -v tesseract >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install --no-install-recommends -y imagemagick
sudo apt-get install --no-install-recommends -y imagemagick tesseract-ocr
fi
convert -version
tesseract --version
- name: Prepare public history bundle with redacted layout previews
id: prepare_public_history

View File

@ -208,8 +208,9 @@ Permanent public history has a narrower boundary. A trusted job without
provider or AWS credentials copies successful declared PNG screenshots into a
separate publication tree. It validates the PNG container, reduces each image
to at most 160 pixels on either edge, applies a strong blur, limits the image to
24 colors, and removes metadata. These layout previews preserve coarse UI
state while making rendered task and provider text unreadable. The job then
24 colors, and removes metadata. It then runs OCR and rejects the whole public
bundle if any letter or digit remains readable. These layout previews preserve
coarse UI state while making rendered task and provider text unreadable. The job then
removes the full-resolution raster files, failure screenshots, video, archives,
SVG, and generated Playwright/blob/HTML report trees. Only the derived
`public-visuals/*.png` previews and allowlisted inert evidence (`.json`, `.log`,
@ -384,8 +385,9 @@ GitHub Pages remains the stable latest dashboard. Enable Pages with GitHub
Actions as its source and set `RUNNER_FULL_STACK_E2E_PUBLISH_PAGES=true`.
The trusted report job creates a separate public bundle before the AWS role is
available. It publishes only blurred, low-resolution, metadata-free layout
previews for successful declared screenshots and allowlisted inert structured
text. The S3/CloudFront history and optional Pages mirror use this same bundle.
previews with an empty OCR result for successful declared screenshots and
allowlisted inert structured text. The S3/CloudFront history and optional Pages
mirror use this same bundle.
Full-resolution and failure screenshots, video, archives, generated reports,
databases, Paperclip homes, workspaces, raw logs, and credentials are never
published. Sanitized allowlisted `.log` copies may be public only after

View File

@ -199,11 +199,13 @@ It accepts only declared screenshots from passing results. It validates a
bounded, non-interlaced PNG container before invoking ImageMagick with strict
memory, disk, thread, and time limits. It reduces each image to at most 160
pixels on either edge, applies a strong blur, limits the palette, removes alpha
and metadata, and validates the new PNG again. This is a layout preview, not
diagnostic evidence. It then prunes full-resolution raster files, all failure
images, video, archives, active SVG, and generated reports. This transformation
runs before AWS credentials are available. The AWS job downloads only the
prepared public tree.
and metadata, and validates the new PNG again. Tesseract must then find no
readable letter or digit. OCR output is never logged. Any readable text or OCR
failure blocks the whole publication. This is a layout preview, not diagnostic
evidence. The job then prunes full-resolution raster files, all failure images,
video, archives, active SVG, and generated reports. This transformation runs
before AWS credentials are available. The AWS job downloads only the prepared
public tree.
The remaining allowlist contains `public-visuals/*.png`, `.json`, `.log`, `.md`,
and `.txt` evidence, plus the generated dashboard, normalized

View File

@ -263,6 +263,24 @@ async function validatePublicPreview(file: string) {
});
}
async function assertPreviewHasNoReadableText(file: string) {
const { stdout } = await execFileAsync(
process.env.RUNNER_E2E_TESSERACT_BINARY ?? "tesseract",
[file, "stdout", "--psm", "11", "-l", "eng"],
{
timeout: 30_000,
maxBuffer: 1024 * 1024,
env: { ...process.env, OMP_THREAD_LIMIT: "1" },
},
);
// The report job no longer has provider secrets, so it cannot compare OCR
// output with exact credential values. Reject every readable letter or digit
// instead. Do not include OCR output in the error because it is untrusted.
if (/[\p{L}\p{N}]/u.test(stdout)) {
throw new Error("Public layout preview still contains OCR-readable text");
}
}
export async function createPublicLayoutPreview(
source: string,
destination: string,
@ -313,6 +331,7 @@ export async function createPublicLayoutPreview(
{ timeout: 45_000, maxBuffer: 1024 * 1024 },
);
await validatePublicPreview(output);
await assertPreviewHasNoReadableText(output);
await copyFile(output, destination, fsConstants.COPYFILE_EXCL);
} finally {
await rm(temporary, { recursive: true, force: true });

View File

@ -690,6 +690,8 @@ describe("public repository paid workflow security", () => {
expect(workflow).toContain("unexpected_png=");
expect(workflow).toContain("passed_count=");
expect(workflow).toContain("pnpm test:e2e:runner:history:prepare");
expect(report).toContain("tesseract-ocr");
expect(report).toContain("tesseract --version");
expect(report).not.toContain("id-token: write");
expect(report).not.toMatch(
/(?:OPENAI|ANTHROPIC|OPENROUTER|DAYTONA)_API_KEY/,