Skip to content

Commit 45ab44e

Browse files
Merge pull request #99 from auth0/feat/generate-pdf-buffer
feat: add PDF buffer generation via Puppeteer
2 parents edf6fb1 + 99caf86 commit 45ab44e

File tree

2 files changed

+112
-6
lines changed

2 files changed

+112
-6
lines changed

analyzer/report.js

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
const fs = require("fs");
12
const i18n = require("i18n");
3+
const path = require("path");
24
const _ = require("lodash");
5+
const Handlebars = require("handlebars");
6+
const puppeteer = require("puppeteer");
37
const listOfAnalyser = require("./lib/listOfAnalyser");
48
const {
59
getAccessToken,
@@ -24,7 +28,31 @@ const {
2428

2529
const logger = require("./lib/logger");
2630
const { getSummaryReport } = require("./tools/summary");
27-
const { convertToTitleCase, tranformReport } = require("./tools/utils");
31+
const { convertToTitleCase, tranformReport, getToday } = require("./tools/utils");
32+
const { version } = require("../package.json");
33+
34+
i18n.configure({
35+
defaultLocale: "en",
36+
objectNotation: true,
37+
directory: path.join(__dirname, "../locales")
38+
});
39+
40+
Handlebars.registerHelper("chooseFont", function (locale) {
41+
if (locale === "ja") return "Noto Sans JP, sans-serif";
42+
if (locale === "ko") return "Noto Sans KR, sans-serif";
43+
return "DM Sans, sans-serif";
44+
});
45+
Handlebars.registerHelper("replace", function (str, search, replace) {
46+
return str.replace(search, replace);
47+
});
48+
Handlebars.registerHelper("and", (a, b) => a && b);
49+
Handlebars.registerHelper("inc", (a) => parseInt(a) + 1);
50+
51+
const templateData = fs.readFileSync(
52+
path.join(__dirname, "../views/pdf_cli_report.handlebars"),
53+
"utf8"
54+
);
55+
2856
async function runProductionChecks(tenant, validators) {
2957
try {
3058
logger.log("info", "Checking your configuration...");
@@ -170,7 +198,7 @@ async function generateReport(locale, tenantConfig, config) {
170198
cd.message = i18n.__(`checkEmailTemplates.${cd.field}`, cd.value);
171199
});
172200
break;
173-
case "checkErrorPageTemplate":
201+
case "checkErrorPageTemplate":
174202
report.details.forEach((cd) => {
175203
cd.message = i18n.__(`checkErrorPageTemplate.${cd.field}`, cd.value);
176204
});
@@ -307,7 +335,7 @@ async function generateReport(locale, tenantConfig, config) {
307335
});
308336
});
309337
break;
310-
case "checkPasswordResetMFA":
338+
case "checkPasswordResetMFA":
311339
case "checkPreRegistrationUserEnumeration":
312340
case "checkActionsHardCodedValues":
313341
case "checkDASHardCodedValues":
@@ -334,7 +362,7 @@ async function generateReport(locale, tenantConfig, config) {
334362
return `<li>${message}</li>`;
335363
}).join("\n");
336364
const dasTitle = i18n.__(`${report.name}.action_script_title`,
337-
scriptName);
365+
scriptName);
338366
return `<p>${dasTitle}<p>\n<ul>\n${listItems}\n</ul>`;
339367
});
340368

@@ -401,6 +429,84 @@ async function generateReport(locale, tenantConfig, config) {
401429
}
402430
}
403431

432+
async function generatePdfBuffer(report, auth0Domain, locale) {
433+
locale = locale || "en";
434+
const today = await getToday(locale);
435+
436+
const data = { report, auth0Domain, today, locale, version, config: {} };
437+
438+
const browser = await puppeteer.launch({
439+
headless: true, // Run in headless mode
440+
args: [
441+
"--no-sandbox", // Disable the sandbox
442+
"--disable-setuid-sandbox", // Disable setuid sandbox
443+
],
444+
});
445+
446+
try {
447+
const template = Handlebars.compile(templateData);
448+
const htmlContent = template({
449+
locale: data.locale,
450+
data,
451+
preamble: data.report.preamble,
452+
});
453+
const page = await browser.newPage();
454+
455+
// Disable JS execution in the Puppeteer page. Disabling
456+
// JS eliminates the browser-side execution surface during PDF rendering.
457+
await page.setJavaScriptEnabled(false);
458+
459+
// Allowlist only the CDN hostnames the template legitimately loads from
460+
// (Bootstrap, Google Fonts). All other outbound requests — including RFC 1918
461+
// addresses and cloud metadata endpoints — are aborted to prevent SSRF via
462+
// passive resource loading (e.g. <img src="http://169.254.169.254/...">) in
463+
// case tenant-controlled data ever reaches an unescaped template field.
464+
await page.setRequestInterception(true);
465+
page.on("request", (req) => {
466+
const url = new URL(req.url());
467+
const allowed = [
468+
"fonts.googleapis.com",
469+
"fonts.gstatic.com",
470+
"cdn.jsdelivr.net",
471+
];
472+
if (req.resourceType() === "document" || allowed.includes(url.hostname)) {
473+
req.continue();
474+
} else {
475+
req.abort();
476+
}
477+
});
478+
479+
await page.setContent(htmlContent, { waitUntil: "networkidle2" });
480+
481+
const pdfResult = await page.pdf({
482+
format: "A4",
483+
printBackground: true,
484+
displayHeaderFooter: true,
485+
headerTemplate: `<div></div>`,
486+
footerTemplate: `
487+
<div style="font-size:10px; width:100%; padding:10px 0; display:flex; align-items:center; justify-content:space-between; border-top:1px solid #ddd;">
488+
<span style="flex:1; text-align:center;">Confidential. For internal evaluation purposes only.</span>
489+
<span style="flex:1; text-align:right; padding-right:20px;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
490+
</div>`,
491+
margin: {
492+
top: "20px",
493+
bottom: "60px",
494+
},
495+
});
496+
// Puppeteer v20+ returns Uint8Array, not Buffer. Express's res.send() calls
497+
// Buffer.isBuffer() and JSON-serialises anything that fails the check,
498+
// producing {"0":37,"1":80,...} instead of raw binary. Convert explicitly.
499+
500+
return Buffer.from(pdfResult);
501+
} catch (error) {
502+
logger.log("error", `Error generating PDF: ${error}`);
503+
throw error;
504+
} finally {
505+
await browser.close();
506+
}
507+
}
508+
404509
module.exports = {
405510
generateReport,
511+
generatePdfBuffer,
406512
};

locales/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
},
183183
"no_custom_domains": "This tenant is not configured to use a custom domain. We recommend using custom domains with Universal Login for the most seamless and secure experience for end users. We also highly recommend using custom domains for passkey authentication, given they are tied to a specific domain during enrollment.",
184184
"ready": "This tenant is configured to use a custom domain. %s",
185-
"pending_verfification": "The tenant's custom domain configuration is incomplete, %s",
185+
"pending_verification": "The tenant's custom domain configuration is incomplete, %s",
186186
"severity": "High",
187187
"status": "red",
188188
"severity_message": "Configure a Custom Domain",
@@ -1477,4 +1477,4 @@
14771477
"https://auth0.com/docs/customize/events/event-testing-observability-and-failure-recovery"
14781478
]
14791479
}
1480-
}
1480+
}

0 commit comments

Comments
 (0)