xss-defender
Version:
A standalone library for XSS sanitization and detection.
633 lines (603 loc) • 76 kB
JavaScript
"use strict";
(self["webpackChunkngx_xss_defender"] = self["webpackChunkngx_xss_defender"] || []).push([["commons"],{
/***/ "./spec/core/xss-defender.spec.ts":
/*!****************************************!*\
!*** ./spec/core/xss-defender.spec.ts ***!
\****************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _src_index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../src/index */ "./src/index.ts");
// Предполагая, что index.ts экспортирует все необходимое
describe("XssDefender", () => {
let defender;
beforeEach(() => {
// Инициализируем XssDefender с конфигурацией по умолчанию перед каждым тестом
// Отключаем логирование для чистоты вывода тестов
defender = new _src_index__WEBPACK_IMPORTED_MODULE_0__.XssDefender({
..._src_index__WEBPACK_IMPORTED_MODULE_0__.DEFAULT_SANITIZATION_CONFIG,
enableLogging: false,
});
});
describe("sanitizeString", () => {
it("should return an empty string for null or undefined input", () => {
expect(defender.sanitizeString(null)).toBe("");
expect(defender.sanitizeString(undefined)).toBe("");
});
it("should return an empty string for an empty string input", () => {
expect(defender.sanitizeString("")).toBe("");
});
it("should return the same string if it is safe", () => {
const safeString = "This is a safe string with 漢字 and 123.";
expect(defender.sanitizeString(safeString)).toBe(safeString);
});
it("should remove <script> tags by default", () => {
const maliciousString = 'Hello <script>alert("XSS")</script> world!';
const expectedString = "Hello world!"; // Пробелы могут остаться в зависимости от реализации replace
expect(defender.sanitizeString(maliciousString)).toBe(expectedString);
});
it("should remove <script> tags with attributes", () => {
const maliciousString = 'Hello <script type="text/javascript">alert("XSS")</script> world!';
const expectedString = "Hello world!";
expect(defender.sanitizeString(maliciousString)).toBe(expectedString);
});
it("should remove on<event> handlers", () => {
const maliciousString = "<div onclick=\"alert('XSS')\">Click me</div>";
// По умолчанию, div является разрешенным тегом, но onclick должен быть удален
// DANGEROUS_PATTERNS удалит 'onclick="alert(\'XSS\')"' целиком
const expectedString = "<div>Click me</div>"; // Атрибут onclick удален
expect(defender.sanitizeString(maliciousString)).toBe(expectedString);
});
it("should encode disallowed tags if stripIgnoreTag is false", () => {
defender.setConfig({
allowedTags: ["p"],
stripIgnoreTag: false,
enableLogging: false,
});
const htmlString = "<p>Allowed</p><badtag>Disallowed</badtag><img>";
// <script> и другие DANGEROUS_PATTERNS удаляются до обработки тегов
const maliciousScript = '<script>alert("no")</script>'; // будет удалено
const mixedString = `<p>Allowed</p><badtag>Disallowed</badtag><img> ${maliciousScript}`;
const expectedString = "<p>Allowed</p><badtag>Disallowed</badtag><img> ";
expect(defender.sanitizeString(mixedString)).toBe(expectedString);
});
});
describe("hasXssRisks", () => {
it("should return false for safe strings", () => {
expect(defender.hasXssRisks("this is fine")).toBeFalse();
expect(defender.hasXssRisks("")).toBeFalse();
expect(defender.hasXssRisks(null)).toBeFalse();
expect(defender.hasXssRisks(undefined)).toBeFalse();
});
it("should return true for strings with <script> tags", () => {
expect(defender.hasXssRisks("<script>alert('XSS')</script>")).toBeTrue();
});
it("should return true for strings with on<event> handlers", () => {
expect(defender.hasXssRisks('<img src="x" onerror="alert(\'XSS\')">')).toBeTrue();
});
it("should return true for strings with javascript: protocol", () => {
expect(defender.hasXssRisks('href="javascript:alert(1)"')).toBeTrue();
});
it("should return true for strings with data: protocol", () => {
expect(defender.hasXssRisks('src="data:text/html;base64,PHNjcmlwdD4="')).toBeTrue();
});
it("should return true for strings with expression() in style", () => {
expect(defender.hasXssRisks("<div style=\"width: expression(alert('XSS'))\">")).toBeTrue();
});
});
describe("Llama 3.2 tests XssDefender", () => {
it("удаляет классический скрипт", () => {
const payload = "<script>alert(1)</script>";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("обфусцированный регистром JS", () => {
const payload = "<JaVaScRiPt>alert(2)</JAvAsCrIpT>";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("скрытая вставка с комментариями", () => {
const payload = "<scr<!-- -->ipt>alert(3)</scr" + "ipt>";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("использует Unicode-экранирование", () => {
const payload = "<\u0073\u0063\u0072\u0069\u0070\u0074>alert(4)</script>";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("HTML-комментарий внутри тега", () => {
const payload = "<scr<!-- -->ipt>alert(5)</script>";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: простой <script> тег", () => {
const payload = '<script>alert("XSS")</script>';
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: <ScRiPt> смешанный регистр", () => {
const payload = "текст <ScRiPt>alert(1)</ScRiPt> еще текст";
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: javascript: URL, комментарий, регистр", () => {
const payload = '<a hReF="jAvAsCrIpT:alert(1)">link</a>';
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: style expression() с табуляцией", () => {
const payload = '<div style="width: exPreSsIoN(\talert(1))"></div>';
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: svg onload с hex и Unicode", () => {
const payload = "<s\u0076g/onload=&#" + "x61;lert(1)>"; // 'alert'
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: iframe srcdoc с внутренним скриптом", () => {
const payload = '<IFRAME SRCdoc="<img src=1 oNErRoR=alert(1)>"></IFRAME>';
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: data: URL с HTML+JS, регистр", () => {
const payload = 'Href="daTa:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTwvc2NyaXB0Pg=="';
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("hasXssRisks: onerror с неразрывным пробелом", () => {
const payload = '<img src="x" onerror\u00A0="alert(1)">'; // \u00A0 is NBSP
expect(defender.hasXssRisks(payload)).toBeTrue();
});
it("sanitizeString: удаляет <script> смешанный регистр", () => {
const payload = "Начало <ScR\u0049pT>alert(1)</ScR\u0049pT> Конец";
// <script> удаляется DANGEROUS_PATTERNS
expect(defender.sanitizeString(payload)).toBe("Начало Конец");
});
it("sanitizeString: img onerror, Unicode в событии", () => {
const payload = '<img src="x" o\u006Eerror="alert(1)">';
// onerror атрибут удаляется DANGEROUS_PATTERNS или фильтром атрибутов
// 'img' и 'src' разрешены по умолчанию.
expect(defender.sanitizeString(payload)).toBe('<img src="x">');
});
it("sanitizeString: a href javascript: URL, комментарий", () => {
const payload = '<a hrEf="jaVaScRiPt:alert(1)">Test</a>';
// javascript: удаляется из href DANGEROUS_PATTERNS
// 'a' и 'href' разрешены.
expect(defender.sanitizeString(payload)).toBe('<a href="">Test</a>');
});
it("sanitizeString: style expression() с %20 и регистром", () => {
const payload = '<div style="color:%20ExpReSsIoN(alert(1))">Text</div>';
expect(defender.sanitizeString(payload)).toBe('<div style="color:">Text</div>');
});
it("sanitizeString: svg onload, hex в теге", () => {
const payload = '<svg onload="alert(1)"></svg>';
// onload удаляется DANGEROUS_PATTERNS.
// <svg> не разрешен -> кодируется.
expect(defender.sanitizeString(payload)).toBe("<svg ></svg>");
});
it("sanitizeString: iframe src с TAB и JS URL", () => {
const payload = '<IFRAME\tsrc="javascript:alert(1)"></IFRAME>';
// javascript: удаляется DANGEROUS_PATTERNS.
// <iframe> не разрешен -> кодируется.
expect(defender.sanitizeString(payload)).toBe('<IFRAME\tsrc=""></IFRAME>');
});
it("sanitizeString: data: URL в href, Unicode в протоколе", () => {
const payload = '<a hRef="d\u0061Ta:text/html,<script>alert(1)</script>">X</a>';
// data:text/html со <script> удаляется из href DANGEROUS_PATTERNS
expect(defender.sanitizeString(payload)).toBe('<a href="">X</a>');
});
it("sanitizeString: текст и <ScRiPt> %20 кодирование", () => {
const payload = "SafeText <ScRiPt%20sRc='x.js'></ScRiPt%20> End";
// <script> удаляется DANGEROUS_PATTERNS
expect(defender.sanitizeString(payload)).toBe("SafeText End");
});
it("sanitizeString: img OnMousEOVER с HTML-комментарием", () => {
const payload = '<img src="ok.png" OnMousEOVER="alert(1)">';
// OnMousEOVER удаляется DANGEROUS_PATTERNS или фильтром атрибутов.
expect(defender.sanitizeString(payload)).toBe('<img src="ok.png">');
});
it("sanitizeString: ссылка с URL Unicode обфускацией JS", () => {
const payload = '<a hRef="\u006a\u0061\u0076\u0061script:alert(1)">Link</a>';
// javascript: удаляется DANGEROUS_PATTERNS
expect(defender.sanitizeString(payload)).toBe('<a href="">Link</a>');
});
});
});
/***/ }),
/***/ "./src/core/config.ts":
/*!****************************!*\
!*** ./src/core/config.ts ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ DEFAULT_SANITIZATION_CONFIG: () => (/* binding */ DEFAULT_SANITIZATION_CONFIG)
/* harmony export */ });
/**
* Default sanitization configuration.
*/
const DEFAULT_SANITIZATION_CONFIG = {
allowedTags: [
"p",
"br",
"b",
"i",
"ul",
"ol",
"li",
"span",
"div",
"a",
"img",
],
allowedAttributes: ["id", "class", "style", "href", "target", "src"],
stripIgnoreTag: false,
enableLogging: false,
logFormat: "simple",
};
/***/ }),
/***/ "./src/core/logger.ts":
/*!****************************!*\
!*** ./src/core/logger.ts ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Logger: () => (/* binding */ Logger)
/* harmony export */ });
/**
* Logger class for XssDefender activities.
*/
class Logger {
/**
* Logs an informational message.
* @param message The message to log.
* @param config The current sanitization configuration.
*/
log(message, config) {
if (config.enableLogging) {
console.info(`XssDefender: ${message}`);
}
}
/**
* Logs a warning message.
* @param message The warning message.
* @param details Optional details to include in the log (used if logFormat is 'detailed').
* @param config The current sanitization configuration.
*/
warn(message, details, config) {
if (config?.enableLogging) {
if (config.logFormat === "detailed" && details) {
console.warn(`XssDefender: ${message}`, details);
}
else {
console.warn(`XssDefender: ${message}`);
}
}
}
}
/***/ }),
/***/ "./src/core/patterns.ts":
/*!******************************!*\
!*** ./src/core/patterns.ts ***!
\******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ DANGEROUS_PATTERNS: () => (/* binding */ DANGEROUS_PATTERNS)
/* harmony export */ });
/**
* Regular expressions for detecting common XSS attack vectors.
* These patterns are used to identify and remove or neutralize malicious code.
*/
const DANGEROUS_PATTERNS = [
// 1. CSS Expressions (усиленный для пробелов и комментариев)
/(?:\/\*[\s\S]*?\*\/)?(?:\s|%[0-9a-fA-F]{2})*expression[\s\u00A0]*\((?:[^(){};]+|\((?:[^(){};]+|\([^(){};]*\))*\))*\)/gi,
// 2. "on..." event handler attributes (NBSP-aware)
/[\s\u00A0]*on\w+(?:\/\*.*?\*\/)?[\s\u00A0]*=[\s\u00A0]*(?:"(?:(?!"|\\).)*?"|'(?:(?!'|\\).)*?'|[^\s>]*)/gi,
// 3. Script tags - более полный паттерн
/<\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\b[^>]*>[\s\S]*?<\s*\/\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\s*>/gi,
// 3-б) Одиночные <script …> или </script>
/<\s*\/?\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\b[^>]*>/gi,
/<\/?javascript[^>]*>[\s\S]*?<\/javascript[^>]*>/gi,
/<javascript[^>]*>/gi,
/<\/javascript[^>]*>/gi,
// 4. JavaScript protocol (обновленный для Unicode обфускации)
/(?<=href[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|:)+/gi,
/(?<=src[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|:)+/gi,
/(?<=action[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|:)+/gi,
/(?<=formaction[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|:)+/gi,
// Простые версии для javascript:
/javascript[\s\u00A0]*:/gi,
/\\u006a\\u0061\\u0076\\u0061script[\s\u00A0]*:/gi,
// 5. data: protocol для опасных MIME-типов
/(?<=href[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi,
/(?<=src[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi,
/(?<=srcdoc[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi,
// Общий data: для hasXssRisks
/\bdata:/gi,
/\\u0064\\u0061\\u0054\\u0061:/gi, // Unicode обфускация для data:
// 6. Дополнительные паттерны для iframe srcdoc
/srcdoc[\s\u00A0]*=[\s\u00A0]*(?:"[^"]*(?:script|onerror|onload)[^"]*"|'[^']*(?:script|onerror|onload)[^']*')/gi,
// 7. HTML комментарии с script
/<!--[\s\S]*?<\s*\/?\s*s\s*?c\s*?r\s*?i\s*?p\s*?t\b[\s\S]*?-->/gi,
];
/***/ }),
/***/ "./src/core/xss-defender.ts":
/*!**********************************!*\
!*** ./src/core/xss-defender.ts ***!
\**********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ XssDefender: () => (/* binding */ XssDefender)
/* harmony export */ });
/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./config */ "./src/core/config.ts");
/* harmony import */ var _patterns__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./patterns */ "./src/core/patterns.ts");
/* harmony import */ var _logger__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./logger */ "./src/core/logger.ts");
/**
* Main class for XSS sanitization and defense.
* Provides methods to sanitize strings, HTML content, objects, and URL parameters.
*/
class XssDefender {
/**
* Initializes a new instance of the XssDefender.
* @param initialConfig Optional initial configuration to override defaults.
*/
constructor(initialConfig) {
this.currentConfig = { ..._config__WEBPACK_IMPORTED_MODULE_0__.DEFAULT_SANITIZATION_CONFIG, ...initialConfig };
this.logger = new _logger__WEBPACK_IMPORTED_MODULE_2__.Logger();
if (this.currentConfig.enableLogging) {
this.logger.log("XssDefender initialized.", this.currentConfig);
}
}
/**
* Updates the current sanitization configuration.
* @param config Partial configuration object to merge with the current settings.
*/
setConfig(config) {
this.currentConfig = { ...this.currentConfig, ...config };
if (this.currentConfig.enableLogging) {
this.logger.log("Configuration updated.", this.currentConfig);
}
}
/**
* Retrieves the current sanitization configuration.
* @returns A read-only copy of the current configuration.
*/
getConfig() {
return this.currentConfig;
}
/**
* Performs basic sanitization of a string value based on the provided configuration.
* This includes stripping dangerous patterns, and handling allowed/disallowed tags and attributes.
* @param value The string to sanitize.
* @param config The sanitization configuration to use.
* @returns The sanitized string.
*/
_basicSanitization(value, config) {
let sanitized = this.decodeNumericEntities(value);
sanitized = sanitized.replace(/(\s|\u00A0)+on\w+(?:\/\*[\s\S]*?\*\/)?[\s\u00A0]*=\s*(?:(["']).*?\2|[^\s>]+)/gi, "$1");
// 1. Remove known dangerous patterns
_patterns__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_PATTERNS.forEach((pattern) => {
sanitized = sanitized.replace(pattern, "");
});
// 2. Handle HTML tags
if (config.allowedTags && config.allowedTags.length > 0) {
// Pattern to find tags that are NOT in the allowed list
const tagsToRemovePattern = new RegExp(`</?(?!(?:${config.allowedTags.join("|")})(?:\\s|>|/))[a-zA-Z0-9]+[^>]*>`, "gi");
sanitized = sanitized.replace(tagsToRemovePattern, (match) => {
if (config.stripIgnoreTag) {
return ""; // Remove the disallowed tag
}
// Encode the disallowed tag
return match.replace(/</g, "<").replace(/>/g, ">");
});
}
else {
// No tags are allowed
if (config.stripIgnoreTag) {
// Remove all tags
sanitized = sanitized.replace(/<[^>]+>/gi, "");
}
else {
// Encode all tags
sanitized = sanitized.replace(/<(\/?[\w\d\s="/.'-]+?)>/gi, (m, tagContent) => `<${tagContent}>`);
}
}
// 3. Handle HTML attributes (only for tags that are allowed or were not stripped)
if (config.allowedAttributes && config.allowedAttributes.length > 0) {
const universalTagPattern = /<([a-zA-Z0-9]+)((?:\s+[^>]*)?)>/g;
sanitized = sanitized.replace(universalTagPattern, (match, tagName, attributesString) => {
const lowerTagName = tagName.toLowerCase();
// Skip if the tag itself is not allowed
if (config.allowedTags &&
!config.allowedTags.includes(lowerTagName)) {
return match;
}
const attributePattern = /\s*([a-zA-Z0-9\-_]+)\s*=\s*(?:(["'])(.*?)\2|([^>\s]+))/g;
let newAttributesString = "";
let attrMatch;
while ((attrMatch = attributePattern.exec(attributesString)) !== null) {
const attributeName = attrMatch[1].toLowerCase();
if (config.allowedAttributes?.includes(attributeName)) {
let attributeValue = attrMatch[3] !== undefined ? attrMatch[3] : attrMatch[4];
const escapedAttributeValue = attributeValue.replace(/"/g, """);
newAttributesString += ` ${attributeName}="${escapedAttributeValue}"`;
}
}
return `<${tagName}${newAttributesString}>`;
});
}
else {
// No attributes allowed, remove all attributes from all tags
const allAttributesPattern = /\s+[a-zA-Z0-9\-_]+\s*=\s*(?:(["']).*?\1|[^>\s]+)/g;
sanitized = sanitized.replace(/<([a-zA-Z0-9]+)((?:\s+[^>]*)?)>/g, (match, tagName, attributesString) => {
return `<${tagName}${attributesString.replace(allAttributesPattern, "")}>`;
});
}
return sanitized;
}
/**
* Sanitizes a string, removing potential XSS threats.
* @param value The string to sanitize. Can be null or undefined, in which case an empty string is returned.
* @returns The sanitized string.
*/
sanitizeString(value) {
if (value === null || value === undefined || value === "")
return "";
const originalValue = String(value);
let sanitizedValue = originalValue;
sanitizedValue = this._basicSanitization(sanitizedValue, this.currentConfig);
if (originalValue !== sanitizedValue) {
this._logDetection(originalValue, sanitizedValue);
}
return sanitizedValue;
}
/**
* Sanitizes an HTML string and sets it as the innerHTML of a given HTMLElement.
* @param element The HTMLElement whose innerHTML is to be set.
* @param unsafeHtml The potentially unsafe HTML string to sanitize and apply.
*/
sanitizeHtmlForElement(element, unsafeHtml) {
if (!element)
return;
const sanitizedHtml = this.sanitizeString(unsafeHtml);
element.innerHTML = sanitizedHtml;
}
/**
* Recursively sanitizes all string values within an object or an array.
* @param obj The object or array to sanitize.
* @returns The sanitized object or array.
*/
sanitizeObject(obj) {
if (obj === null || typeof obj !== "object") {
if (typeof obj === "string") {
return this.sanitizeString(obj);
}
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.sanitizeObject(item));
}
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
if (typeof value === "string") {
result[key] = this.sanitizeString(value);
}
else if (typeof value === "object") {
result[key] = this.sanitizeObject(value);
}
else {
result[key] = value;
}
}
}
return result;
}
/**
* Checks URL parameters for potential XSS risks and returns sanitized versions.
* @param params A record of URL parameters.
* @returns An object indicating if all parameters are safe and a list of any issues found.
*/
checkUrlParams(params) {
const issues = [];
let isSafe = true;
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const paramValue = params[key];
const valuesToCheck = Array.isArray(paramValue)
? paramValue
: paramValue
? [paramValue]
: [];
for (const originalSingleValue of valuesToCheck) {
if (typeof originalSingleValue !== "string")
continue;
const sanitizedSingleValue = this.sanitizeString(originalSingleValue);
// An issue is logged if sanitization changed the value OR if the original value had detectable XSS patterns
// (even if sanitizeString somehow missed it or the patterns are different)
if (originalSingleValue !== sanitizedSingleValue ||
this.hasXssRisks(originalSingleValue)) {
if (originalSingleValue !== sanitizedSingleValue) {
// Prefer logging if actual change occurred
isSafe = false;
issues.push({
key,
value: sanitizedSingleValue,
originalValue: originalSingleValue,
});
this.logger.warn(`Potential XSS risk in URL parameter "${key}" was sanitized.`, {
key,
originalValue: originalSingleValue,
sanitizedValue: sanitizedSingleValue,
timestamp: new Date().toISOString(),
}, this.currentConfig);
}
else if (this.hasXssRisks(originalSingleValue)) {
// Log if original had risk, even if sanitization resulted in same string (less likely)
isSafe = false; // Still mark as not entirely safe due to initial risk
issues.push({
key,
value: sanitizedSingleValue,
originalValue: originalSingleValue,
}); // Report original and "sanitized"
this.logger.warn(`Potential XSS risk detected in URL parameter "${key}". Input was already clean or sanitization was ineffective.`, {
key,
originalValue: originalSingleValue,
sanitizedValue: sanitizedSingleValue,
timestamp: new Date().toISOString(),
}, this.currentConfig);
}
}
}
}
}
return { isSafe, issues };
}
/**
* Checks if a given string contains known XSS risk patterns.
* This method tests the string against `DANGEROUS_PATTERNS`.
* @param value The string to check. Can be null or undefined.
* @returns `true` if XSS risks are found, `false` otherwise.
*/
hasXssRisks(value) {
if (!value)
return false;
const str = String(value);
return _patterns__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_PATTERNS.some((p) => new RegExp(p.source, p.flags).test(str));
}
decodeNumericEntities(str) {
return (str
// &#xNNNN;
.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
// &#NNNN;
.replace(/&#(\d+);?/g, (_, dec) => String.fromCharCode(parseInt(dec, 10))));
}
/**
* Logs a detection event when sanitization modifies the input string.
* @param originalValue The original, unsafe string.
* @param sanitizedValue The sanitized string.
*/
_logDetection(originalValue, sanitizedValue) {
const details = {
originalValue,
sanitizedValue,
configUsed: {
// Log only key aspects of config for brevity unless detailed
allowedTags: this.currentConfig.allowedTags,
allowedAttributes: this.currentConfig.allowedAttributes,
stripIgnoreTag: this.currentConfig.stripIgnoreTag,
},
timestamp: new Date().toISOString(),
};
this.logger.warn("Potential XSS detected and input sanitized.", details, this.currentConfig);
}
}
/***/ }),
/***/ "./src/index.ts":
/*!**********************!*\
!*** ./src/index.ts ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ DEFAULT_SANITIZATION_CONFIG: () => (/* reexport safe */ _core_config__WEBPACK_IMPORTED_MODULE_1__.DEFAULT_SANITIZATION_CONFIG),
/* harmony export */ XssDefender: () => (/* reexport safe */ _core_xss_defender__WEBPACK_IMPORTED_MODULE_0__.XssDefender)
/* harmony export */ });
/* harmony import */ var _core_xss_defender__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./core/xss-defender */ "./src/core/xss-defender.ts");
/* harmony import */ var _core_config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./core/config */ "./src/core/config.ts");
/***/ })
}]);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbW9ucy5qcyIsIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQUEyRSxDQUFDO0FBQzVFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSx1QkFBdUIsbURBQVc7QUFDbEMsZUFBZSxtRUFBMkI7QUFDMUM7QUFDQSxTQUFTO0FBQ1QsS0FBSztBQUNMO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQSxvREFBb0Q7QUFDcEQ7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0E7QUFDQSwwREFBMEQ7QUFDMUQ7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQWE7QUFDYjtBQUNBO0FBQ0Esb0VBQW9FO0FBQ3BFLGtGQUFrRixnQkFBZ0I7QUFDbEcsc0RBQXNELFVBQVUsY0FBYyxXQUFXLElBQUksUUFBUTtBQUNyRztBQUNBLFNBQVM7QUFDVCxLQUFLO0FBQ0w7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0EsNkRBQTZEO0FBQzdELFNBQVM7QUFDVDtBQUNBO0FBQ0EsU0FBUztBQUNULEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0EseURBQXlELFdBQVc7QUFDcEU7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0Esa0RBQWtEO0FBQ2xEO0FBQ0EsU0FBUztBQUNUO0FBQ0Esc0VBQXNFO0FBQ3RFO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQSxTQUFTO0FBQ1Q7QUFDQSxxQ0FBcUMsNkJBQTZCO0FBQ2xFO0FBQ0E7QUFDQSwrREFBK0QsUUFBUSxJQUFJLFFBQVE7QUFDbkYsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsK0RBQStELGtCQUFrQixJQUFJLFdBQVc7QUFDaEcsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNUO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBUztBQUNULEtBQUs7QUFDTCxDQUFDOzs7Ozs7Ozs7Ozs7Ozs7QUN4TEQ7QUFDQTtBQUNBO0FBQ087QUFDUDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7Ozs7Ozs7Ozs7OztBQ3JCQTtBQUNBO0FBQ0E7QUFDTztBQUNQO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EseUNBQXlDLFFBQVE7QUFDakQ7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLDZDQUE2QyxRQUFRO0FBQ3JEO0FBQ0E7QUFDQSw2Q0FBNkMsUUFBUTtBQUNyRDtBQUNBO0FBQ0E7QUFDQTs7Ozs7Ozs7Ozs7Ozs7O0FDOUJBO0FBQ0E7QUFDQTtBQUNBO0FBQ087QUFDUDtBQUNBLDZDQUE2QyxFQUFFLG1DQUFtQyxlQUFlLFlBQVk7QUFDN0c7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSwySkFBMko7QUFDM0osMEpBQTBKO0FBQzFKLDZKQUE2SjtBQUM3SixpS0FBaUs7QUFDaks7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7QUNuQ3VEO0FBQ1A7QUFDZDtBQUNsQztBQUNBO0FBQ0E7QUFDQTtBQUNPO0FBQ1A7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLCtCQUErQixHQUFHLGdFQUEyQjtBQUM3RCwwQkFBMEIsMkNBQU07QUFDaEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsK0JBQStCO0FBQy9CO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsUUFBUSx5REFBa0I7QUFDMUI7QUFDQSxTQUFTO0FBQ1Q7QUFDQTtBQUNBO0FBQ0EsK0RBQStELDZCQUE2QjtBQUM1RjtBQUNBO0FBQ0EsK0JBQStCO0FBQy9CO0FBQ0E7QUFDQSxnREFBZ0Qsc0JBQXNCO0FBQ3RFLGFBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxtR0FBbUcsRUFBRSxXQUFXLElBQUk7QUFDcEg7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSwwRkFBMEY7QUFDMUYsbURBQW1ELGNBQWMsSUFBSSxzQkFBc0I7QUFDM0Y7QUFDQTtBQUNBLDJCQUEyQixRQUFRLEVBQUUsb0JBQW9CO0FBQ3pELGFBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsMkJBQTJCLFFBQVEsRUFBRSxtREFBbUQ7QUFDeEYsYUFBYTtBQUNiO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLDZCQUE2QjtBQUM3QixxRkFBcUYsSUFBSTtBQUN6RjtBQUNBO0FBQ0E7QUFDQTtBQUNBLDZCQUE2QjtBQUM3QjtBQUNBO0FBQ0E7QUFDQSw0Q0FBNEM7QUFDNUM7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2QkFBNkIsR0FBRztBQUNoQyw4RkFBOEYsSUFBSTtBQUNsRztBQUNBO0FBQ0E7QUFDQTtBQUNBLDZCQUE2QjtBQUM3QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCO0FBQ2pCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxlQUFlLHlEQUFrQjtBQUNqQztBQUNBO0FBQ0E7QUFDQTtBQUNBLHFDQUFxQztBQUNyQztBQUNBLDhCQUE4QjtBQUM5QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhO0FBQ2I7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FDdlFrRDtBQUNVIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NwZWMvY29yZS94c3MtZGVmZW5kZXIuc3BlYy50cyIsIndlYnBhY2s6Ly9uZ3gteHNzLWRlZmVuZGVyLy4vc3JjL2NvcmUvY29uZmlnLnRzIiwid2VicGFjazovL25neC14c3MtZGVmZW5kZXIvLi9zcmMvY29yZS9sb2dnZXIudHMiLCJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NyYy9jb3JlL3BhdHRlcm5zLnRzIiwid2VicGFjazovL25neC14c3MtZGVmZW5kZXIvLi9zcmMvY29yZS94c3MtZGVmZW5kZXIudHMiLCJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NyYy9pbmRleC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBYc3NEZWZlbmRlciwgREVGQVVMVF9TQU5JVElaQVRJT05fQ09ORklHIH0gZnJvbSBcIi4uLy4uL3NyYy9pbmRleFwiOyAvLyDQn9GA0LXQtNC/0L7Qu9Cw0LPQsNGPLCDRh9GC0L4gaW5kZXgudHMg0Y3QutGB0L/QvtGA0YLQuNGA0YPQtdGCINCy0YHQtSDQvdC10L7QsdGF0L7QtNC40LzQvtC1XG5kZXNjcmliZShcIlhzc0RlZmVuZGVyXCIsICgpID0+IHtcbiAgICBsZXQgZGVmZW5kZXI7XG4gICAgYmVmb3JlRWFjaCgoKSA9PiB7XG4gICAgICAgIC8vINCY0L3QuNGG0LjQsNC70LjQt9C40YDRg9C10LwgWHNzRGVmZW5kZXIg0YEg0LrQvtC90YTQuNCz0YPRgNCw0YbQuNC10Lkg0L/QviDRg9C80L7Qu9GH0LDQvdC40Y4g0L/QtdGA0LXQtCDQutCw0LbQtNGL0Lwg0YLQtdGB0YLQvtC8XG4gICAgICAgIC8vINCe0YLQutC70Y7Rh9Cw0LXQvCDQu9C+0LPQuNGA0L7QstCw0L3QuNC1INC00LvRjyDRh9C40YHRgtC+0YLRiyDQstGL0LLQvtC00LAg0YLQtdGB0YLQvtCyXG4gICAgICAgIGRlZmVuZGVyID0gbmV3IFhzc0RlZmVuZGVyKHtcbiAgICAgICAgICAgIC4uLkRFRkFVTFRfU0FOSVRJWkFUSU9OX0NPTkZJRyxcbiAgICAgICAgICAgIGVuYWJsZUxvZ2dpbmc6IGZhbHNlLFxuICAgICAgICB9KTtcbiAgICB9KTtcbiAgICBkZXNjcmliZShcInNhbml0aXplU3RyaW5nXCIsICgpID0+IHtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIGFuIGVtcHR5IHN0cmluZyBmb3IgbnVsbCBvciB1bmRlZmluZWQgaW5wdXRcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG51bGwpKS50b0JlKFwiXCIpO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHVuZGVmaW5lZCkpLnRvQmUoXCJcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gYW4gZW1wdHkgc3RyaW5nIGZvciBhbiBlbXB0eSBzdHJpbmcgaW5wdXRcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKFwiXCIpKS50b0JlKFwiXCIpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRoZSBzYW1lIHN0cmluZyBpZiBpdCBpcyBzYWZlXCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHNhZmVTdHJpbmcgPSBcIlRoaXMgaXMgYSBzYWZlIHN0cmluZyB3aXRoIOa8ouWtlyBhbmQgMTIzLlwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHNhZmVTdHJpbmcpKS50b0JlKHNhZmVTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmVtb3ZlIDxzY3JpcHQ+IHRhZ3MgYnkgZGVmYXVsdFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBtYWxpY2lvdXNTdHJpbmcgPSAnSGVsbG8gPHNjcmlwdD5hbGVydChcIlhTU1wiKTwvc2NyaXB0PiB3b3JsZCEnO1xuICAgICAgICAgICAgY29uc3QgZXhwZWN0ZWRTdHJpbmcgPSBcIkhlbGxvICB3b3JsZCFcIjsgLy8g0J/RgNC+0LHQtdC70Ysg0LzQvtCz0YPRgiDQvtGB0YLQsNGC0YzRgdGPINCyINC30LDQstC40YHQuNC80L7RgdGC0Lgg0L7RgiDRgNC10LDQu9C40LfQsNGG0LjQuCByZXBsYWNlXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcobWFsaWNpb3VzU3RyaW5nKSkudG9CZShleHBlY3RlZFN0cmluZyk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZW1vdmUgPHNjcmlwdD4gdGFncyB3aXRoIGF0dHJpYnV0ZXNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgbWFsaWNpb3VzU3RyaW5nID0gJ0hlbGxvIDxzY3JpcHQgdHlwZT1cInRleHQvamF2YXNjcmlwdFwiPmFsZXJ0KFwiWFNTXCIpPC9zY3JpcHQ+IHdvcmxkISc7XG4gICAgICAgICAgICBjb25zdCBleHBlY3RlZFN0cmluZyA9IFwiSGVsbG8gIHdvcmxkIVwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1hbGljaW91c1N0cmluZykpLnRvQmUoZXhwZWN0ZWRTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmVtb3ZlIG9uPGV2ZW50PiBoYW5kbGVyc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBtYWxpY2lvdXNTdHJpbmcgPSBcIjxkaXYgb25jbGljaz1cXFwiYWxlcnQoJ1hTUycpXFxcIj5DbGljayBtZTwvZGl2PlwiO1xuICAgICAgICAgICAgLy8g0J/QviDRg9C80L7Qu9GH0LDQvdC40Y4sIGRpdiDRj9Cy0LvRj9C10YLRgdGPINGA0LDQt9GA0LXRiNC10L3QvdGL0Lwg0YLQtdCz0L7QvCwg0L3QviBvbmNsaWNrINC00L7Qu9C20LXQvSDQsdGL0YLRjCDRg9C00LDQu9C10L1cbiAgICAgICAgICAgIC8vIERBTkdFUk9VU19QQVRURVJOUyDRg9C00LDQu9C40YIgJ29uY2xpY2s9XCJhbGVydChcXCdYU1NcXCcpXCInINGG0LXQu9C40LrQvtC8XG4gICAgICAgICAgICBjb25zdCBleHBlY3RlZFN0cmluZyA9IFwiPGRpdj5DbGljayBtZTwvZGl2PlwiOyAvLyDQkNGC0YDQuNCx0YPRgiBvbmNsaWNrINGD0LTQsNC70LXQvVxuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1hbGljaW91c1N0cmluZykpLnRvQmUoZXhwZWN0ZWRTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgZW5jb2RlIGRpc2FsbG93ZWQgdGFncyBpZiBzdHJpcElnbm9yZVRhZyBpcyBmYWxzZVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBkZWZlbmRlci5zZXRDb25maWcoe1xuICAgICAgICAgICAgICAgIGFsbG93ZWRUYWdzOiBbXCJwXCJdLFxuICAgICAgICAgICAgICAgIHN0cmlwSWdub3JlVGFnOiBmYWxzZSxcbiAgICAgICAgICAgICAgICBlbmFibGVMb2dnaW5nOiBmYWxzZSxcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgY29uc3QgaHRtbFN0cmluZyA9IFwiPHA+QWxsb3dlZDwvcD48YmFkdGFnPkRpc2FsbG93ZWQ8L2JhZHRhZz48aW1nPlwiO1xuICAgICAgICAgICAgLy8gPHNjcmlwdD4g0Lgg0LTRgNGD0LPQuNC1IERBTkdFUk9VU19QQVRURVJOUyDRg9C00LDQu9GP0Y7RgtGB0Y8g0LTQviDQvtCx0YDQsNCx0L7RgtC60Lgg0YLQtdCz0L7QslxuICAgICAgICAgICAgY29uc3QgbWFsaWNpb3VzU2NyaXB0ID0gJzxzY3JpcHQ+YWxlcnQoXCJub1wiKTwvc2NyaXB0Pic7IC8vINCx0YPQtNC10YIg0YPQtNCw0LvQtdC90L5cbiAgICAgICAgICAgIGNvbnN0IG1peGVkU3RyaW5nID0gYDxwPkFsbG93ZWQ8L3A+PGJhZHRhZz5EaXNhbGxvd2VkPC9iYWR0YWc+PGltZz4gJHttYWxpY2lvdXNTY3JpcHR9YDtcbiAgICAgICAgICAgIGNvbnN0IGV4cGVjdGVkU3RyaW5nID0gXCI8cD5BbGxvd2VkPC9wPiZsdDtiYWR0YWcmZ3Q7RGlzYWxsb3dlZCZsdDsvYmFkdGFnJmd0OyZsdDtpbWcmZ3Q7IFwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1peGVkU3RyaW5nKSkudG9CZShleHBlY3RlZFN0cmluZyk7XG4gICAgICAgIH0pO1xuICAgIH0pO1xuICAgIGRlc2NyaWJlKFwiaGFzWHNzUmlza3NcIiwgKCkgPT4ge1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gZmFsc2UgZm9yIHNhZmUgc3RyaW5nc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoXCJ0aGlzIGlzIGZpbmVcIikpLnRvQmVGYWxzZSgpO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiXCIpKS50b0JlRmFsc2UoKTtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhudWxsKSkudG9CZUZhbHNlKCk7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3ModW5kZWZpbmVkKSkudG9CZUZhbHNlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIDxzY3JpcHQ+IHRhZ3NcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiPHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD5cIikpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIG9uPGV2ZW50PiBoYW5kbGVyc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoJzxpbWcgc3JjPVwieFwiIG9uZXJyb3I9XCJhbGVydChcXCdYU1NcXCcpXCI+JykpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIGphdmFzY3JpcHQ6IHByb3RvY29sXCIsICgpID0+IHtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcygnaHJlZj1cImphdmFzY3JpcHQ6YWxlcnQoMSlcIicpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRydWUgZm9yIHN0cmluZ3Mgd2l0aCBkYXRhOiBwcm90b2NvbFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoJ3NyYz1cImRhdGE6dGV4dC9odG1sO2Jhc2U2NCxQSE5qY21sd2REND1cIicpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRydWUgZm9yIHN0cmluZ3Mgd2l0aCBleHByZXNzaW9uKCkgaW4gc3R5bGVcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiPGRpdiBzdHlsZT1cXFwid2lkdGg6IGV4cHJlc3Npb24oYWxlcnQoJ1hTUycpKVxcXCI+XCIpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICB9KTtcbiAgICBkZXNjcmliZShcIkxsYW1hIDMuMiB0ZXN0cyBYc3NEZWZlbmRlclwiLCAoKSA9PiB7XG4gICAgICAgIGl0KFwi0YPQtNCw0LvRj9C10YIg0LrQu9Cw0YHRgdC40YfQtdGB0LrQuNC5INGB0LrRgNC40L/RglwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8c2NyaXB0PmFsZXJ0KDEpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcItC+0LHRhNGD0YHRhtC40YDQvtCy0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgNC+0LwgSlNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9IFwiPEphVmFTY1JpUHQ+YWxlcnQoMik8L0pBdkFzQ3JJcFQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcItGB0LrRgNGL0YLQsNGPINCy0YHRgtCw0LLQutCwINGBINC60L7QvNC80LXQvdGC0LDRgNC40Y/QvNC4XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSBcIjxzY3I8IS0tIC0tPmlwdD5hbGVydCgzKTwvc2NyXCIgKyBcImlwdD5cIjtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwi0LjRgdC/0L7Qu9GM0LfRg9C10YIgVW5pY29kZS3RjdC60YDQsNC90LjRgNC+0LLQsNC90LjQtVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8XFx1MDA3M1xcdTAwNjNcXHUwMDcyXFx1MDA2OVxcdTAwNzBcXHUwMDc0PmFsZXJ0KDQpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcIkhUTUwt0LrQvtC80LzQtdC90YLQsNGA0LjQuSDQstC90YPRgtGA0Lgg0YLQtdCz0LBcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9IFwiPHNjcjwhLS0gLS0+aXB0PmFsZXJ0KDUpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcImhhc1hzc1Jpc2tzOiDQv9GA0L7RgdGC0L7QuSA8c2NyaXB0PiDRgtC10LNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8c2NyaXB0PmFsZXJ0KFwiWFNTXCIpPC9zY3JpcHQ+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IDxTY1JpUHQ+INGB0LzQtdGI0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCLRgtC10LrRgdGCIDxTY1JpUHQ+YWxlcnQoMSk8L1NjUmlQdD4g0LXRidC1INGC0LXQutGB0YJcIjtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IGphdmFzY3JpcHQ6IFVSTCwg0LrQvtC80LzQtdC90YLQsNGA0LjQuSwg0YDQtdCz0LjRgdGC0YBcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8YSBoUmVGPVwiakF2QXNDcklwVDphbGVydCgxKVwiPmxpbms8L2E+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IHN0eWxlIGV4cHJlc3Npb24oKSDRgSDRgtCw0LHRg9C70Y/RhtC40LXQuVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxkaXYgc3R5bGU9XCJ3aWR0aDogZXhQcmVTc0lvTihcXHRhbGVydCgxKSlcIj48L2Rpdj4nO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKHBheWxvYWQpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJoYXNYc3NSaXNrczogc3ZnIG9ubG9hZCDRgSBoZXgg0LggVW5pY29kZVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8c1xcdTAwNzZnL29ubG9hZD0mI1wiICsgXCJ4NjE7bGVydCgxKT5cIjsgLy8gJ2FsZXJ0J1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKHBheWxvYWQpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJoYXNYc3NSaXNrczogaWZyYW1lIHNyY2RvYyDRgSDQstC90YPRgtGA0LXQvdC90LjQvCDRgdC60YDQuNC/0YLQvtC8XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSAnPElGUkFNRSBTUkNkb2M9XCI8aW1nIHNyYz0xIG9ORXJSb1I9YWxlcnQoMSk+XCI+PC9JRlJBTUU+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IGRhdGE6IFVSTCDRgSBIVE1MK0pTLCDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJ0hyZWY9XCJkYVRhOnRleHQvaHRtbDtiYXNlNjQsUEhOamNtbHdkRDVoYkdWeWRDZ2lTR1ZzYkc4aUtUd3ZjMk55YVhCMFBnPT1cIic7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcImhhc1hzc1Jpc2tzOiBvbmVycm9yINGBINC90LXRgNCw0LfRgNGL0LLQvdGL0Lwg0L/RgNC+0LHQtdC70L7QvFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxpbWcgc3JjPVwieFwiIG9uZXJyb3JcXHUwMEEwPVwiYWxlcnQoMSlcIj4nOyAvLyBcXHUwMEEwIGlzIE5CU1BcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6INGD0LTQsNC70Y/QtdGCIDxzY3JpcHQ+INGB0LzQtdGI0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCLQndCw0YfQsNC70L4gPFNjUlxcdTAwNDlwVD5hbGVydCgxKTwvU2NSXFx1MDA0OXBUPiDQmtC+0L3QtdGGXCI7XG4gICAgICAgICAgICAvLyA8c2NyaXB0PiDRg9C00LDQu9GP0LXRgtGB0Y8gREFOR0VST1VTX1BBVFRFUk5TXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoXCLQndCw0YfQsNC70L4gINCa0L7QvdC10YZcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNhbml0aXplU3RyaW5nOiBpbWcgb25lcnJvciwgVW5pY29kZSDQsiDRgdC+0LHRi9GC0LjQuFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxpbWcgc3JjPVwieFwiIG9cXHUwMDZFZXJyb3I9XCJhbGVydCgxKVwiPic7XG4gICAgICAgICAgICAvLyBvbmVycm9yINCw0YLRgNC40LHRg9GCINGD0LTQsNC70Y/QtdGC0YHRjyBEQU5HRVJPVVNfUEFUVEVSTlMg0LjQu9C4INGE0LjQu9GM0YLRgNC+0Lwg0LDRgtGA0LjQsdGD0YLQvtCyXG4gICAgICAgICAgICAvLyAnaW1nJyDQuCAnc3JjJyDRgNCw0LfRgNC10YjQtdC90Ysg0L/QviDRg9C80L7Qu9GH0LDQvdC40Y4uXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJzxpbWcgc3JjPVwieFwiPicpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzYW5pdGl6ZVN0cmluZzogYSBocmVmIGphdmFzY3JpcHQ6IFVSTCwg0LrQvtC80LzQtdC90YLQsNGA0LjQuVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxhIGhyRWY9XCJqYVZhU2NSaVB0OmFsZXJ0KDEpXCI+VGVzdDwvYT4nO1xuICAgICAgICAgICAgLy8gamF2YXNjcmlwdDog0YPQtNCw0LvRj9C10YLRgdGPINC40LcgaHJlZiBEQU5HRVJPVVNfUEFUVEVSTlNcbiAgICAgICAgICAgIC8vICdhJyDQuCAnaHJlZicg0YDQsNC30YDQtdGI0LXQvdGLLlxuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHBheWxvYWQpKS50b0JlKCc8YSBocmVmPVwiXCI+VGVzdDwvYT4nKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IHN0eWxlIGV4cHJlc3Npb24oKSDRgSAlMjAg0Lgg0YDQtdCz0LjRgdGC0YDQvtC8XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSAnPGRpdiBzdHlsZT1cImNvbG9yOiUyMEV4cFJlU3NJb04oYWxlcnQoMSkpXCI+VGV4dDwvZGl2Pic7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJzxkaXYgc3R5bGU9XCJjb2xvcjpcIj5UZXh0PC9kaXY+Jyk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNhbml0aXplU3RyaW5nOiBzdmcgb25sb2FkLCBoZXgg0LIg0YLQtdCz0LVcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8cyYjeDc2O2cgb25sb2FkPVwiYWxlcnQoMSlcIj48L3MmI3g3NjtnPic7XG4gICAgICAgICAgICAvLyBvbmxvYWQg0YPQtNCw0LvRj9C10YLRgdGPIERBTkdFUk9VU19QQVRURVJOUy5cbiAgICAgICAgICAgIC8vIDxzdmc+INC90LUg0YDQsNC30YDQtdGI0LXQvSAtPiDQutC+0LTQuNGA0YPQtdGC0YHRjy5cbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5zYW5pdGl6ZVN0cmluZyhwYXlsb2FkKSkudG9CZShcIiZsdDtzdmcgJmd0OyZsdDsvc3ZnJmd0O1wiKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IGlmcmFtZSBzcmMg0YEgVEFCINC4IEpTIFVSTFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxJRlJBTUVcXHRzcmM9XCJqYXZhc2NyaXB0OmFsZXJ0KDEpXCI+PC9JRlJBTUU+JztcbiAgICAgICAgICAgIC8vIGphdmFzY3JpcHQ6INGD0LTQsNC70Y/QtdGC0YHRjyBEQU5HRVJPVVNfUEFUVEVSTlMuXG4gICAgICAgICAgICAvLyA8aWZyYW1lPiDQvdC1INGA0LDQt9GA0LXRiNC10L0gLT4g0LrQvtC00LjRgNGD0LXRgtGB0Y8uXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJyZsdDtJRlJBTUVcXHRzcmM9XCJcIiZndDsmbHQ7L0lGUkFNRSZndDsnKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IGRhdGE6IFVSTCDQsiBocmVmLCBVbmljb2RlINCyINC/0YDQvtGC0L7QutC+0LvQtVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxhIGhSZWY9XCJkXFx1MDA2MVRhOnRleHQvaHRtbCw8c2NyaXB0PmFsZXJ0KDEpPC9zY3JpcHQ+XCI+WDwvYT4nO1xuICAgICAgICAgICAgLy8gZGF0YTp0ZXh0L2h0bWwg0YHQviA8c2NyaXB0PiDRg9C00LDQu9GP0LXRgtGB0Y8g0LjQtyBocmVmIERBTkdFUk9VU19QQVRURVJOU1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHBheWxvYWQ