@govtechsg/oobee 0.10.20 → 0.10.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -346,6 +346,24 @@ Options:
346
346
  ks
347
347
  [string] [choices: "default", "disable-oobee", "enable-wcag-aaa", "disable-oob
348
348
  ee,enable-wcag-aaa"] [default: "default"]
349
+ -g, --generateJsonFiles Generate two JSON files containing the
350
+ results of the accessibility scan:
351
+ 1. `scanData.json`: Provides an overview of
352
+ the scan, including:
353
+ - WCAG compliance score
354
+ - Violated WCAG clauses
355
+ - Metadata (e.g., scan start and end times)
356
+ - Pages scanned and skipped
357
+ 2. `scanItems.json`: Contains detailed
358
+ information about detected accessibility
359
+ issues, including:
360
+ - Severity levels
361
+ - Issue descriptions
362
+ - Related WCAG guidelines
363
+ - URL of the pages violated the WCAG clauses
364
+ Useful for in-depth analysis or integration
365
+ with external reporting tools.
366
+ [string] [choices: "yes", "no"] [default: "no"]
349
367
 
350
368
  Examples:
351
369
  To scan sitemap of website:', 'npm run cli -- -c [ 1 | sitemap ] -u <url_lin
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@govtechsg/oobee",
3
3
  "main": "dist/npmIndex.js",
4
- "version": "0.10.20",
4
+ "version": "0.10.21",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@json2csv/node": "^7.0.3",
@@ -93,4 +93,4 @@
93
93
  "url": "https://github.com/GovTechSG/oobee/issues"
94
94
  },
95
95
  "homepage": "https://github.com/GovTechSG/oobee#readme"
96
- }
96
+ }
package/src/cli.ts CHANGED
@@ -184,11 +184,10 @@ Usage: npm run cli -- -c <crawler> -d <device> -w <viewport> -u <url> OPTIONS`,
184
184
  return option;
185
185
  })
186
186
  .check(argvs => {
187
- if (
188
- (argvs.scanner === ScannerTypes.CUSTOM || argvs.scanner === ScannerTypes.LOCALFILE) &&
189
- argvs.maxpages
190
- ) {
191
- throw new Error('-p or --maxpages is only available in website and sitemap scans.');
187
+ if (argvs.scanner === ScannerTypes.CUSTOM && argvs.maxpages) {
188
+ throw new Error(
189
+ '-p or --maxpages is only available in website, sitemap and local file scans.',
190
+ );
192
191
  }
193
192
  return true;
194
193
  })
@@ -394,7 +393,9 @@ const optionsAnswer: Answers = {
394
393
  blacklistedPatternsFilename: options.blacklistedPatternsFilename,
395
394
  playwrightDeviceDetailsObject: options.playwrightDeviceDetailsObject,
396
395
  ruleset: options.ruleset,
396
+ generateJsonFiles: options.generateJsonFiles,
397
397
  };
398
+
398
399
  await scanInit(optionsAnswer);
399
400
  process.exit(0);
400
401
 
package/src/combine.ts CHANGED
@@ -48,18 +48,19 @@ const combineRun = async (details: Data, deviceToScan: string) => {
48
48
  maxRequestsPerCrawl,
49
49
  browser,
50
50
  userDataDirectory,
51
- strategy,
52
- specifiedMaxConcurrency,
51
+ strategy, // Allow subdomains: if checked, = 'same-domain'
52
+ specifiedMaxConcurrency, // Slow scan mode: if checked, = '1'
53
53
  fileTypes,
54
54
  blacklistedPatternsFilename,
55
- includeScreenshots,
56
- followRobots,
55
+ includeScreenshots, // Include screenshots: if checked, = 'true'
56
+ followRobots, // Adhere to robots.txt: if checked, = 'true'
57
57
  metadata,
58
58
  customFlowLabel = 'Custom Flow',
59
59
  extraHTTPHeaders,
60
60
  safeMode,
61
61
  zip,
62
- ruleset,
62
+ ruleset, // Enable custom checks, Enable WCAG AAA: if checked, = 'enable-wcag-aaa')
63
+ generateJsonFiles,
63
64
  } = envDetails;
64
65
 
65
66
  process.env.CRAWLEE_LOG_LEVEL = 'ERROR';
@@ -90,6 +91,12 @@ const combineRun = async (details: Data, deviceToScan: string) => {
90
91
  crawlType: type,
91
92
  requestUrl: finalUrl,
92
93
  urlsCrawled: new UrlsCrawled(),
94
+ isIncludeScreenshots: envDetails.includeScreenshots,
95
+ isAllowSubdomains: envDetails.strategy,
96
+ isEnableCustomChecks: envDetails.ruleset,
97
+ isEnableWcagAaa: envDetails.ruleset,
98
+ isSlowScanMode: envDetails.specifiedMaxConcurrency,
99
+ isAdhereRobots: envDetails.followRobots,
93
100
  };
94
101
 
95
102
  const viewportSettings: ViewportSettingsClass = new ViewportSettingsClass(
@@ -214,6 +221,7 @@ const combineRun = async (details: Data, deviceToScan: string) => {
214
221
  undefined,
215
222
  scanDetails,
216
223
  zip,
224
+ generateJsonFiles,
217
225
  );
218
226
  const [name, email] = nameEmail.split(':');
219
227
 
@@ -270,7 +270,9 @@ export const cliOptions: { [key: string]: Options } = {
270
270
  coerce: option => {
271
271
  const validChoices = Object.values(RuleFlags);
272
272
  const userChoices: string[] = option.split(',');
273
- const invalidUserChoices = userChoices.filter(choice => !validChoices.includes(choice as RuleFlags));
273
+ const invalidUserChoices = userChoices.filter(
274
+ choice => !validChoices.includes(choice as RuleFlags),
275
+ );
274
276
  if (invalidUserChoices.length > 0) {
275
277
  printMessage(
276
278
  [
@@ -294,6 +296,27 @@ export const cliOptions: { [key: string]: Options } = {
294
296
  return userChoices;
295
297
  },
296
298
  },
299
+ g: {
300
+ alias: 'generateJsonFiles',
301
+ describe:
302
+ 'Generate JSON files in the results folder. Accepts "yes", "no", "y", or "n". Default is "no".',
303
+ type: 'string',
304
+ requiresArg: true,
305
+ default: 'no',
306
+ demandOption: false,
307
+ coerce: value => {
308
+ const validYes = ['yes', 'y'];
309
+ const validNo = ['no', 'n'];
310
+
311
+ if (validYes.includes(value.toLowerCase())) {
312
+ return true;
313
+ }
314
+ if (validNo.includes(value.toLowerCase())) {
315
+ return false;
316
+ }
317
+ throw new Error(`Invalid value "${value}" for --generate. Use "yes", "y", "no", or "n".`);
318
+ },
319
+ },
297
320
  };
298
321
 
299
322
  export const configureReportSetting = (isEnabled: boolean): void => {
@@ -579,6 +579,7 @@ export const prepareData = async (argv: Answers): Promise<Data> => {
579
579
  safeMode,
580
580
  zip,
581
581
  ruleset,
582
+ generateJsonFiles,
582
583
  } = argv;
583
584
 
584
585
  // construct filename for scan results
@@ -625,6 +626,7 @@ export const prepareData = async (argv: Answers): Promise<Data> => {
625
626
  safeMode,
626
627
  zip,
627
628
  ruleset,
629
+ generateJsonFiles,
628
630
  };
629
631
  };
630
632
 
@@ -581,18 +581,11 @@ const crawlDomain = async ({
581
581
  },
582
582
  ]
583
583
  : [
584
- async (crawlingContext, gotoOptions) => {
585
- const { page, request } = crawlingContext;
586
-
584
+ async ({ page, request }) => {
587
585
  await page.setExtraHTTPHeaders({
588
586
  ...extraHTTPHeaders,
589
587
  });
590
588
 
591
- Object.assign(gotoOptions, {
592
- waitUntil: 'networkidle',
593
- timeout: 30000,
594
- });
595
-
596
589
  const processible = await isProcessibleUrl(request.url);
597
590
  if (!processible) {
598
591
  request.skipNavigation = true;
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type Answers = {
51
51
  exportDirectory: string;
52
52
  zip: string;
53
53
  ruleset: RuleFlags[];
54
+ generateJsonFiles: boolean;
54
55
  };
55
56
 
56
57
  export type Data = {
@@ -80,6 +81,7 @@ export type Data = {
80
81
  userDataDirectory?: string;
81
82
  zip?: string;
82
83
  ruleset: RuleFlags[];
84
+ generateJsonFiles: boolean;
83
85
  };
84
86
 
85
87
  const userData = getUserDataTxt();
@@ -24,8 +24,6 @@ import { consoleLogger, silentLogger } from './logs.js';
24
24
  import itemTypeDescription from './constants/itemTypeDescription.js';
25
25
  import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
26
26
 
27
- const cwd = process.cwd();
28
-
29
27
  export type ItemsInfo = {
30
28
  html: string;
31
29
  message: string;
@@ -84,10 +82,12 @@ type AllIssues = {
84
82
  cypressScanAboutMetadata: string;
85
83
  wcagLinks: { [key: string]: string };
86
84
  [key: string]: any;
85
+ advancedScanOptionsSummaryItems: { [key: string]: boolean };
87
86
  };
88
87
 
89
88
  const filename = fileURLToPath(import.meta.url);
90
89
  const dirname = path.dirname(filename);
90
+ const BUFFER_LIMIT = 100 * 1024 * 1024; // 100MB size
91
91
 
92
92
  const extractFileNames = async (directory: string): Promise<string[]> => {
93
93
  ensureDirSync(directory);
@@ -214,8 +214,8 @@ const compileHtmlWithEJS = async (allIssues, storagePath, htmlFilename = 'report
214
214
  const injectScript = `
215
215
  <script>
216
216
  try {
217
- const base64DecodeChunkedWithDecoder = (data, chunkSize = 1024 * 1024) => {
218
- const encodedChunks = data.split('.');
217
+ const base64DecodeChunkedWithDecoder = (data, chunkSize = ${BUFFER_LIMIT}) => {
218
+ const encodedChunks = data.split('|');
219
219
  const decoder = new TextDecoder();
220
220
  const jsonParts = [];
221
221
 
@@ -259,7 +259,7 @@ const splitHtmlAndCreateFiles = async (htmlFilePath, storagePath) => {
259
259
  throw new Error('Marker comment not found in the HTML file.');
260
260
  }
261
261
 
262
- const topContent = htmlContent.slice(0, splitIndex + splitMarker.length) + '\n\n';
262
+ const topContent = `${htmlContent.slice(0, splitIndex + splitMarker.length)}\n\n`;
263
263
  const bottomContent = htmlContent.slice(splitIndex + splitMarker.length);
264
264
 
265
265
  const topFilePath = path.join(storagePath, 'report-partial-top.htm.txt');
@@ -293,8 +293,6 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report') => {
293
293
 
294
294
  outputStream.write(prefixData);
295
295
 
296
- // Create a readable stream for the input file with a highWaterMark set to 10MB
297
- const BUFFER_LIMIT = 10 * 1024 * 1024; // 10 MB
298
296
  const inputStream = fs.createReadStream(inputFilePath, {
299
297
  encoding: 'utf-8',
300
298
  highWaterMark: BUFFER_LIMIT,
@@ -457,15 +455,14 @@ function writeLargeJsonToFile(obj, filePath) {
457
455
  });
458
456
  }
459
457
 
460
- const base64Encode = async (data, num) => {
458
+ const base64Encode = async (data, num, storagePath, generateJsonFiles) => {
461
459
  try {
462
- const tempFilename =
460
+ const tempFilePath =
463
461
  num === 1
464
- ? `scanItems_${uuidv4()}.json`
462
+ ? path.join(storagePath, 'scanItems.json')
465
463
  : num === 2
466
- ? `scanData_${uuidv4()}.json`
467
- : `${uuidv4()}.json`;
468
- const tempFilePath = path.join(process.cwd(), tempFilename);
464
+ ? path.join(storagePath, 'scanData.json')
465
+ : path.join(storagePath, `${uuidv4()}.json`);
469
466
 
470
467
  await writeLargeJsonToFile(data, tempFilePath);
471
468
 
@@ -473,16 +470,28 @@ const base64Encode = async (data, num) => {
473
470
  const outputFilePath = path.join(process.cwd(), outputFilename);
474
471
 
475
472
  try {
476
- const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
477
473
  const readStream = fs.createReadStream(tempFilePath, {
478
474
  encoding: 'utf8',
479
- highWaterMark: CHUNK_SIZE,
475
+ highWaterMark: BUFFER_LIMIT,
480
476
  });
481
477
  const writeStream = fs.createWriteStream(outputFilePath, { encoding: 'utf8' });
482
478
 
479
+ let previousChunk = null;
480
+
483
481
  for await (const chunk of readStream) {
484
482
  const encodedChunk = Buffer.from(chunk).toString('base64');
485
- writeStream.write(`${encodedChunk}.`);
483
+
484
+ if (previousChunk !== null) {
485
+ // Note: Notice the pipe symbol `|`, it is intended to be here as a delimiter
486
+ // for the scenario where there are chunking happens
487
+ writeStream.write(`${previousChunk}|`);
488
+ }
489
+
490
+ previousChunk = encodedChunk;
491
+ }
492
+
493
+ if (previousChunk !== null) {
494
+ writeStream.write(previousChunk);
486
495
  }
487
496
 
488
497
  await new Promise((resolve, reject) => {
@@ -492,9 +501,11 @@ const base64Encode = async (data, num) => {
492
501
 
493
502
  return outputFilePath;
494
503
  } finally {
495
- await fs.promises
496
- .unlink(tempFilePath)
497
- .catch(err => console.error('Temp file delete error:', err));
504
+ if (!generateJsonFiles) {
505
+ await fs.promises
506
+ .unlink(tempFilePath)
507
+ .catch(err => console.error('Temp file delete error:', err));
508
+ }
498
509
  }
499
510
  } catch (error) {
500
511
  console.error('Error encoding data to Base64:', error);
@@ -520,10 +531,10 @@ const streamEncodedDataToFile = async (inputFilePath, writeStream, appendComma)
520
531
  }
521
532
  };
522
533
 
523
- const writeBase64 = async (allIssues, storagePath) => {
534
+ const writeBase64 = async (allIssues, storagePath, generateJsonFiles) => {
524
535
  const { items, ...rest } = allIssues;
525
- const encodedScanItemsPath = await base64Encode(items, 1);
526
- const encodedScanDataPath = await base64Encode(rest, 2);
536
+ const encodedScanItemsPath = await base64Encode(items, 1, storagePath, generateJsonFiles);
537
+ const encodedScanDataPath = await base64Encode(rest, 2, storagePath, generateJsonFiles);
527
538
 
528
539
  const filePath = path.join(storagePath, 'scanDetails.csv');
529
540
  const directoryPath = path.dirname(filePath);
@@ -766,6 +777,7 @@ const generateArtifacts = async (
766
777
  cypressScanAboutMetadata,
767
778
  scanDetails,
768
779
  zip = undefined, // optional
780
+ generateJsonFiles = false,
769
781
  ) => {
770
782
  const intermediateDatasetsPath = `${randomToken}/datasets/${randomToken}`;
771
783
  const phAppVersion = getVersion();
@@ -835,6 +847,17 @@ const generateArtifacts = async (
835
847
  },
836
848
  cypressScanAboutMetadata,
837
849
  wcagLinks: constants.wcagLinks,
850
+ // Populate boolean values for id="advancedScanOptionsSummary"
851
+ advancedScanOptionsSummaryItems: {
852
+ showIncludeScreenshots: [true].includes(scanDetails.isIncludeScreenshots),
853
+ showAllowSubdomains: [true].includes(scanDetails.isAllowSubdomains),
854
+ showEnableCustomChecks: ['default', 'enable-wcag-aaa'].includes(
855
+ scanDetails.isEnableCustomChecks?.[0],
856
+ ),
857
+ showEnableWcagAaa: (scanDetails.isEnableWcagAaa || []).includes('enable-wcag-aaa'),
858
+ showSlowScanMode: [1].includes(scanDetails.isSlowScanMode),
859
+ showAdhereRobots: [true].includes(scanDetails.isAdhereRobots),
860
+ },
838
861
  };
839
862
 
840
863
  const allFiles = await extractFileNames(intermediateDatasetsPath);
@@ -870,6 +893,9 @@ const generateArtifacts = async (
870
893
  }
871
894
 
872
895
  allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations);
896
+ consoleLogger.info(
897
+ `advancedScanOptionsSummaryItems is ${allIssues.advancedScanOptionsSummaryItems}`,
898
+ );
873
899
 
874
900
  const getAxeImpactCount = (allIssues: AllIssues) => {
875
901
  const impactCount = {
@@ -908,7 +934,7 @@ const generateArtifacts = async (
908
934
  }
909
935
 
910
936
  await writeCsv(allIssues, storagePath);
911
- await writeBase64(allIssues, storagePath);
937
+ await writeBase64(allIssues, storagePath, generateJsonFiles);
912
938
  await writeSummaryHTML(allIssues, storagePath);
913
939
  await writeHTML(allIssues, storagePath);
914
940
  await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length), 1);
@@ -225,6 +225,71 @@
225
225
  </svg>
226
226
  </span>
227
227
  </li>
228
+
229
+ <li>
230
+ <svg aria-label="Advanced options scan summary" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
231
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" stroke="#93928D" stroke-width="1.74"/>
232
+ <path d="M5 9L7.5 11.5L13 6" stroke="#93928D" stroke-width="1.74" stroke-linecap="square"/>
233
+ </svg>
234
+ <div>
235
+ <div class="d-flex flex-row justify-content-center align-items-center gap-3">
236
+ <button id="advancedScanOptionsSummaryTitle" onclick="toggleAdvanceScanSummary()">Advanced scan summary
237
+ </button>
238
+ </div>
239
+
240
+ </div>
241
+ </li>
242
+ <ul id="advancedScanOptionsSummary" class="d-none mb-3">
243
+ <li id="showIncludeScreenshots"class="d-flex flex-row">
244
+ <svg aria-label="Include screenshots was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
245
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
246
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
247
+ </svg>
248
+ <div>Include screenshots
249
+ </div>
250
+ </li>
251
+ <li id="showAllowSubdomains"class="d-flex flex-row">
252
+ <svg aria-label="Allow subdomains was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
253
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
254
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
255
+ </svg>
256
+ <div>Allow subdomains for scans
257
+ </div>
258
+ </li>
259
+ <li id="showEnableCustomChecks"class="d-flex flex-row">
260
+ <svg aria-label="Enable custom checks was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
261
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
262
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
263
+ </svg>
264
+ <div>Enable custom checks
265
+ </div>
266
+ </li>
267
+ <li id="showEnableWcagAaa"class="d-flex flex-row">
268
+ <svg aria-label="Enable WCAG AAA checks was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
269
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
270
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
271
+ </svg>
272
+ <div>Enable WCAG AAA checks
273
+ </div>
274
+ </li>
275
+ <li id="showSlowScanMode"class="d-flex flex-row">
276
+ <svg aria-label="Slow scan mode was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
277
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
278
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
279
+ </svg>
280
+ <div>Slow scan mode
281
+ </div>
282
+ </li>
283
+
284
+ <li id="showAdhereRobots"class="d-flex flex-row">
285
+ <svg aria-label="Adhere to robots.txt was checked" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
286
+ <rect x="0.87" y="0.87" width="16.26" height="16.26" rx="3.13" fill="#93928D" stroke="#93928D" stroke-width="1.74"/>
287
+ <path d="M5 9L7.5 11.5L13 6" fill="none" stroke="#ffffff" stroke-width="1.74" stroke-linecap="square"/>
288
+ </svg>
289
+ <div>Adhere to robots.txt
290
+ </div>
291
+ </li>
292
+ </ul>
228
293
  <li>
229
294
  <svg
230
295
  aria-label="Scan engine"
@@ -0,0 +1,38 @@
1
+ <%# functions to handle interaction and ui for advancedScanOptionsSummary in scanAbout.ejs.
2
+ component %>
3
+
4
+ <script>
5
+ let optionsToCheck = scanData.advancedScanOptionsSummaryItems;
6
+
7
+ document.querySelectorAll('#advancedScanOptionsSummary li').forEach(liElement => {
8
+ liElement.classList.add('d-none');
9
+ });
10
+
11
+ function toggleAdvanceScanSummary() {
12
+ const chevron = document.getElementById('advancedScanOptionsSummaryTitle');
13
+ const advancedScanOptionsSummary = document.getElementById('advancedScanOptionsSummary');
14
+
15
+ const isHidden = advancedScanOptionsSummary.classList.toggle('d-none');
16
+
17
+ chevron.classList.toggle('chevron-rotated', !isHidden);
18
+
19
+ if (!isHidden) {
20
+ showScanOptions(optionsToCheck);
21
+ }
22
+ }
23
+
24
+ function showScanOptions(options) {
25
+ document.querySelectorAll('#advancedScanOptionsSummary li').forEach(liElement => {
26
+ liElement.classList.add('d-none');
27
+ });
28
+
29
+ for (const key in options) {
30
+ if (options[key] === true) {
31
+ const liElement = document.getElementById(key);
32
+ if (liElement) {
33
+ liElement.classList.remove('d-none');
34
+ }
35
+ }
36
+ }
37
+ }
38
+ </script>
@@ -755,7 +755,8 @@
755
755
  margin: 1.5rem 0 1rem 0;
756
756
  }
757
757
 
758
- button#wcagModalToggle {
758
+ button#wcagModalToggle,
759
+ button#advancedScanOptionsSummaryTitle {
759
760
  background: none;
760
761
  border: 0;
761
762
  padding: 0;
@@ -962,6 +963,26 @@
962
963
  width: 1.125rem;
963
964
  }
964
965
 
966
+ #advancedScanOptionsSummary li > svg {
967
+ margin-left: 2rem;
968
+ }
969
+
970
+ #advancedScanOptionsSummaryTitle::after {
971
+ content: '';
972
+ display: inline-block;
973
+ width: 12px;
974
+ height: 12px;
975
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 16" fill="none"><path d="M1.03847 16C0.833084 16 0.632306 15.9388 0.461529 15.8241C0.290753 15.7095 0.157649 15.5465 0.0790493 15.3558C0.000449621 15.1651 -0.0201154 14.9553 0.0199549 14.7529C0.0600251 14.5505 0.158931 14.3645 0.304165 14.2186L6.49293 7.99975L0.304165 1.78088C0.109639 1.58514 0.000422347 1.31979 0.000518839 1.04315C0.000615331 0.766523 0.110018 0.501248 0.30468 0.30564C0.499341 0.110032 0.763331 9.70251e-05 1.03862 6.41929e-08C1.31392 -9.68968e-05 1.57798 0.109652 1.77278 0.305123L8.69586 7.26187C8.8906 7.45757 9 7.72299 9 7.99975C9 8.2765 8.8906 8.54192 8.69586 8.73763L1.77278 15.6944C1.67646 15.7914 1.562 15.8684 1.43598 15.9208C1.30996 15.9733 1.17487 16.0002 1.03847 16Z" fill="%23006B8C" transform="rotate(90, 4.5, 8)"/></svg>');
976
+ background-size: contain;
977
+ background-repeat: no-repeat;
978
+ transform: scaleY(1);
979
+ margin-left: 0.5rem;
980
+ }
981
+
982
+ #advancedScanOptionsSummaryTitle.chevron-rotated::after {
983
+ transform: scaleY(-1);
984
+ }
985
+
965
986
  #footer {
966
987
  padding: 0.75rem 1rem;
967
988
  box-shadow: 0 -0.25rem 10px #736ccb1a;
@@ -25,6 +25,7 @@
25
25
  <%- include('partials/header') %> <%- include('partials/main') %> <%-
26
26
  include('partials/scripts/popper') %> <%- include('partials/scripts/bootstrap') %> <%-
27
27
  include('partials/scripts/highlightjs') %> <%- include('partials/scripts/utils') %> <%-
28
+ include('partials/scripts/scanAboutScript') %> <%-
28
29
  include('partials/scripts/categorySelectorDropdownScript') %> <%-
29
30
  include('partials/scripts/categorySummary') %> <%- include('partials/scripts/ruleOffcanvas') %>
30
31
  <%- include('partials/scripts/screenshotLightbox')%>