1+ const fs = require ( "fs" ) ;
12const i18n = require ( "i18n" ) ;
3+ const path = require ( "path" ) ;
24const _ = require ( "lodash" ) ;
5+ const Handlebars = require ( "handlebars" ) ;
6+ const puppeteer = require ( "puppeteer" ) ;
37const listOfAnalyser = require ( "./lib/listOfAnalyser" ) ;
48const {
59 getAccessToken,
@@ -24,7 +28,31 @@ const {
2428
2529const logger = require ( "./lib/logger" ) ;
2630const { 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+
2856async 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+
404509module . exports = {
405510 generateReport,
511+ generatePdfBuffer,
406512} ;
0 commit comments