From 073a23f6c4d6fee21aecb4f0b46d295ac4ff42ab Mon Sep 17 00:00:00 2001
From: implexrr <84114379+implexrr@users.noreply.github.com>
Date: Wed, 11 Nov 2026 17:32:22 -0500
Subject: [PATCH] feat: label verification and sign-in scan results
---
README.md | 2 ++
scanner/gateway/readiness-origin.mjs | 12 ++++++++++++
scanner/public-report.mjs | 25 +++++++++++++++++++++++++
scanner/public-worker.mjs | 5 +++--
scripts/preview-browser.mjs | 11 +++++++++++
server/publicScanner.integration.ts | 7 +++++++
server/scanner/publicPolicy.test.ts | 16 ++++++++++++++++
server/scanner/publicScanner.ts | 11 +++++++++--
shared/scanPageNotice.ts | 15 +++++++++++++++
shared/types.ts | 2 ++
src/App.tsx | 5 ++++-
src/components/reportExport.ts | 5 +++++
12 files changed, 111 insertions(+), 5 deletions(-)
create mode 100644 shared/scanPageNotice.ts
diff --git a/README.md b/README.md
index eb925b3..e01d43e 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,8 @@ Images are independently validated and embedded in sandboxed, opaque-origin ifra
| POST | `/api/scan-fixture` | Scan a fixed built-in page |
| POST | `/api/analyze` | Explicit illustrative example; never fetches its URL |
+For successful scans, KindPath labels clearly identifiable browser verification and sign-in screens in the report and preview. The API and JSON download include an optional `pageNotice` code and a plain-language limitation. Detection uses the rendered page and can miss unfamiliar screens. Findings and previews describe the page KindPath received.
+
A successful real scan has `mode: "public"`, `requestedUrl`, final `url`, `scanId`, findings, scanner metadata and coverage. `sitePreview` contains the same identity, `kind: "image"`, dimensions, truncation flag, capture timestamp, guarded HTML and limitations. If capture alone fails, `previewUnavailable: true` replaces `sitePreview`; the report limitations explain this. Cleanup failure overrides successful results and capture/status explanations.
## Invite-only staging
diff --git a/scanner/gateway/readiness-origin.mjs b/scanner/gateway/readiness-origin.mjs
index c00d5e1..a4b0d7a 100644
--- a/scanner/gateway/readiness-origin.mjs
+++ b/scanner/gateway/readiness-origin.mjs
@@ -44,6 +44,18 @@ function handle(req,res){
const blocks=Array.from({length:count},(_,index)=>`
Controlled paragraph ${index}
`).join('\n');
res.writeHead(200,{'Content-Type':'text/html'});res.end(retentionPage.replace('',blocks));return;
}
+ const noticePage = {
+ '/verification-screen': 'example.com is verifying your browser…
',
+ '/sign-in-screen': 'Welcome
Sign in
',
+ '/ordinary-sign-in-link': 'Checking your browser
' +
+ 'Learn how to check your browser version.' +
+ '
',
+ }[path];
+ if (typeof noticePage === 'string') {
+ res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
+ res.end('Controlled scan' + noticePage);
+ return;
+ }
if(path==='/missing'){res.writeHead(404,{'Content-Type':'text/plain'});res.end('Controlled missing page');return;}
if(path==='/redirect' && req.headers.host==='example.com'){res.writeHead(302,{Location:'http://www.example.com/redirect'});res.end();return;}
if(path==='/blocked-redirect'||path==='/subresource-redirect'){res.writeHead(302,{Location:'http://10.0.0.1/private'});res.end();return;}
diff --git a/scanner/public-report.mjs b/scanner/public-report.mjs
index 14be3dc..e3c42b3 100644
--- a/scanner/public-report.mjs
+++ b/scanner/public-report.mjs
@@ -26,3 +26,28 @@ export function summarizeRules(rules) {
})),
};
}
+
+// Runs inside the guarded browser. Return only a fixed code, never page text.
+export function detectScanPageNotice() {
+ const normalize = text => text.trim().replace(/\s+/g, ' ').toLowerCase();
+ const visible = element => element.checkVisibility({ opacityProperty: true, visibilityProperty: true });
+ const main = document.querySelector('main, [role="main"]') ?? document.body;
+ if (!main || !visible(main)) return undefined;
+
+ const heading = [...main.querySelectorAll('h1')].find(visible);
+ const headingText = heading ? normalize(heading.innerText) : '';
+ const verification = /^(?:checking your browser|verify (?:that )?you are human|verifying (?:your browser|that you are human)|(?:[a-z0-9.-]+ )?is verifying your browser)[.!…]*$/;
+ const rendered = main.innerText;
+ if (rendered.length <= 200) {
+ const text = normalize(rendered);
+ const supportingText = text.startsWith(headingText) ? text.slice(headingText.length).trim() : '';
+ if (verification.test(text) || (verification.test(headingText) && /^(?:please wait|this may take a few seconds)\b/.test(supportingText))) {
+ return 'browser-verification';
+ }
+ }
+
+ if (/^(?:sign in|log in|login)(?: to (?:your account|[a-z0-9][a-z0-9 .&'-]{0,60}))?[.!]?$/.test(headingText)) {
+ if ([...main.querySelectorAll('form input[type="password"]')].some(visible)) return 'sign-in';
+ }
+ return undefined;
+}
diff --git a/scanner/public-worker.mjs b/scanner/public-worker.mjs
index 3e2f31c..c0c5c59 100644
--- a/scanner/public-worker.mjs
+++ b/scanner/public-worker.mjs
@@ -9,7 +9,7 @@ import { chromium } from 'playwright';
import { validateTarget } from './gateway/policy.mjs';
import { navigationRejection } from './gateway/navigation.mjs';
import { installNavigationGuard } from './gateway/navigation-guard.mjs';
-import { analyzePublicPage, summarizeRules } from './public-report.mjs';
+import { analyzePublicPage, detectScanPageNotice, summarizeRules } from './public-report.mjs';
const socketPath = '/run/kindpath/egress.sock';
const allowed = JSON.parse(process.env.KINDPATH_ALLOWED_HOSTS ?? '[]');
@@ -170,6 +170,7 @@ try {
validateTarget(scannedUrl, allowed);
const scannedDocument=await page.evaluateHandle(()=>document);
stage = 'accessibility-analysis';
+ const pageNotice = await page.evaluate(detectScanPageNotice).catch(() => undefined);
const result = await analyzePublicPage(page);
// Do not label findings from one document with another URL, including same-URL reloads.
if (navigationGuard.failed || navigationGuard.failure || redirectFailure || pageChanged || page.url() !== scannedUrl || result.url !== scannedUrl || !(await page.evaluate(original=>original===document,scannedDocument))) {
@@ -184,7 +185,7 @@ try {
stage = 'worker-result';
const violations = summarizeRules(result.violations);
const incomplete = summarizeRules(result.incomplete);
- process.stdout.write(JSON.stringify({ fixture: 'public', url: scannedUrl, ...(pageLoadIncomplete?{pageLoadIncomplete:true}:{}), ...(preview ? {preview} : {}),...(captureFailure ? {captureFailure} : {}),
+ process.stdout.write(JSON.stringify({ fixture: 'public', url: scannedUrl, ...(pageNotice ? { pageNotice } : {}), ...(pageLoadIncomplete?{pageLoadIncomplete:true}:{}), ...(preview ? {preview} : {}),...(captureFailure ? {captureFailure} : {}),
coverage: { blockedRequests, failedRequests, omittedRules: violations.omittedRules + incomplete.omittedRules,
omittedNodes: violations.omittedNodes + incomplete.omittedNodes },
engine: result.testEngine.version, violations: violations.rules, incomplete: incomplete.rules,
diff --git a/scripts/preview-browser.mjs b/scripts/preview-browser.mjs
index ed193db..0782402 100644
--- a/scripts/preview-browser.mjs
+++ b/scripts/preview-browser.mjs
@@ -86,6 +86,7 @@ try {
assert.equal(exported.report.generatedAt, expected.generatedAt);
assert.deepEqual(exported.report.coverage,coverage ?? undefined);
assert.equal(exported.report.pageLoadIncomplete,expected.pageLoadIncomplete);
+ assert.equal(exported.report.pageNotice,expected.pageNotice);
assert.deepEqual(exported.report.findings, expected.findings);
assert.deepEqual(exported.report.scanner, expected.scanner);
assert.deepEqual(exported.report.manualChecks, expected.manualChecks);
@@ -708,10 +709,20 @@ try {
await publicButton.click();await page.locator('.results').waitFor();
await page.getByRole('region',{name:'Scan coverage',exact:true}).getByText(/Unfinished loads were stopped, so this partial report/).waitFor();
await assertDownload(publicResponse);
+ for (const [pageNotice, screen] of [['browser-verification', 'browser verification'], ['sign-in', 'sign-in']]) {
+ publicResponse = {...publicReport, pageNotice, sitePreview: data.sitePreview};
+ await publicButton.click();
+ const notice = page.getByRole('status').filter({hasText: `KindPath received a ${screen} screen. The findings and any preview describe that screen.`});
+ await notice.waitFor();
+ await page.getByRole('button', {name: /Visual preview/}).click();
+ await notice.waitFor();
+ await assertDownload(publicResponse);
+ }
// Browser-rendered previews: decoded pixels, fitting, scroll access, filters and inert hover areas.
publicResponse={...publicReport,scanId:data.imagePreview.scanId,sitePreview:data.imagePreview};
await publicButton.click();await page.locator('.results').waitFor();
await page.getByRole('button',{name:/Visual preview/}).click();
+ assert.equal(await page.getByRole('status').filter({hasText: /KindPath received a .* screen/}).count(), 0);
const imageFrames=page.locator('iframe');
for(let i=0;i<2;i++){
const frame=page.frameLocator('iframe').nth(i);
diff --git a/server/publicScanner.integration.ts b/server/publicScanner.integration.ts
index 010966e..782b39e 100644
--- a/server/publicScanner.integration.ts
+++ b/server/publicScanner.integration.ts
@@ -72,6 +72,9 @@ for (const scenario of [
{path:'/indented-content',final:'/indented-content'}, {path:'/indented-budget',final:'/indented-budget'},
{path:'/components',final:'/components'}, {path:'/body-contrast',final:'/body-contrast'},
{path:'/pending-script',final:'/pending-script'}, {path:'/slow-script',final:'/slow-script'}, {path:'/script-pending-load',final:'/pending-script'}, {path:'/pending-document',final:'/pending-document'},
+ {path:'/verification-screen',final:'/verification-screen'},
+ {path:'/sign-in-screen',final:'/sign-in-screen'},
+ {path:'/ordinary-sign-in-link',final:'/ordinary-sign-in-link'},
{path:'/forms',final:'/forms'}, {path:'/raster',final:'/raster'}, {path:'/missing',final:'/missing'},
{path:'/same-url-history',final:'/same-url-history'}, {path:'/same-url-reload',final:'/same-url-reload'},
]) {
@@ -142,6 +145,10 @@ test(`URL scan/capture handles controlled ${scenario.path} without public access
}
assert.equal(report.mode, 'public'); assert.equal(report.requestedUrl, target); assert.equal(report.url,'http://'+(scenario.path==='/redirect'?'www.example.com':'example.com')+scenario.final);
if (scenario.path === '/redirect') for (const id of ['image-alt','label','button-name']) assert.ok(report.findings.some((finding: {id:string}) => finding.id === id));
+ const pageNotice = scenario.path === '/verification-screen' ? 'browser-verification'
+ : scenario.path === '/sign-in-screen' ? 'sign-in' : undefined;
+ assert.equal(report.pageNotice, pageNotice);
+ if (pageNotice) assert.ok(report.limitations.some((text: string) => text.includes('describe that screen')));
assert.ok(report.limitations.some((text: string) => /requests were blocked by policy/.test(text)));
assert.equal(lookups, scenario.path==='/redirect'?2:1); assert.equal(PUBLIC_SCANNING_ENABLED, false);
const { PUBLIC_CAPTURE_ENABLED } = await import('./scanner/publicPolicy.js'); assert.equal(PUBLIC_CAPTURE_ENABLED,false);
diff --git a/server/scanner/publicPolicy.test.ts b/server/scanner/publicPolicy.test.ts
index 4b00f06..e8ed532 100644
--- a/server/scanner/publicPolicy.test.ts
+++ b/server/scanner/publicPolicy.test.ts
@@ -54,3 +54,19 @@ test('partial page loading is explicit and malformed loading markers are rejecte
assert.equal(mapPublicReport(raw).pageLoadIncomplete,undefined);
for(const value of [false,null,'true',1,{}])assert.throws(()=>mapPublicReport({...raw,pageLoadIncomplete:value}));
});
+
+
+test('public report page notices accept only fixed own codes', () => {
+ const raw = { fixture: 'public', url: 'https://example.com/', engine: 'test', violations: [],
+ incomplete: [], passes: 1, inapplicable: 0, coverage: { blockedRequests: 0, failedRequests: 0, omittedRules: 0, omittedNodes: 0 } };
+ assert.equal(mapPublicReport(raw).pageNotice, undefined);
+ for (const pageNotice of ['sign-in', 'browser-verification']) {
+ const report = mapPublicReport({ ...raw, pageNotice });
+ assert.equal(report.pageNotice, pageNotice);
+ assert.ok(report.limitations.some(text => text.includes('describe that screen')));
+ }
+ for (const pageNotice of ['constructor', '__proto__', '