diff --git a/.github/workflows/api-binary-tests.yml b/.github/workflows/api-binary-tests.yml index 4ceadb6f5..56914a710 100644 --- a/.github/workflows/api-binary-tests.yml +++ b/.github/workflows/api-binary-tests.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Get repository metadata id: repo @@ -91,7 +91,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Run mock Keycloak id: idp-run diff --git a/api/launchers/stig-manager.bat b/api/launchers/stig-manager.bat index 629bd04f8..0c48e3b57 100644 --- a/api/launchers/stig-manager.bat +++ b/api/launchers/stig-manager.bat @@ -80,6 +80,19 @@ ::============================================================================== :: set STIGMAN_CLASSIFICATION= +::============================================================================== +:: STIGMAN_CLIENT_ADMIN_TIMEOUT +:: +:: | Default: "0" | The maximum time (in minutes) a user with admin privileges +:: can be inactive in the web client before discarding their access token and +:: requiring reauthorization. Activity is defined as mouse click, keypress, or +:: scrolling in any tab or window of a same-origin browsing context group. Set +:: to zero to disable idle detection. +:: +:: Affects: Client +::============================================================================== +:: set STIGMAN_CLIENT_ADMIN_TIMEOUT= + ::============================================================================== :: STIGMAN_CLIENT_API_BASE :: @@ -90,6 +103,16 @@ ::============================================================================== :: set STIGMAN_CLIENT_API_BASE= +::============================================================================== +:: STIGMAN_CLIENT_CONSOLE_MODE +:: +:: | Default: "production" | The console mode of the web client, setting to +:: "development" enables console logging which is otherwise disabled +:: +:: Affects: Client +::============================================================================== +:: set STIGMAN_CLIENT_CONSOLE_MODE= + ::============================================================================== :: STIGMAN_CLIENT_DIRECTORY :: @@ -143,19 +166,6 @@ ::============================================================================== :: set STIGMAN_CLIENT_ID= -::============================================================================== -:: STIGMAN_CLIENT_ADMIN_TIMEOUT -:: -:: | Default: "0" | The maximum time (in minutes) a user with admin privileges -:: can be inactive in the web client before discarding their access token and -:: requiring reauthorization. Activity is defined as mouse click, keypress, or -:: scrolling in any tab or window of a same-origin browsing context group. Set -:: to zero to disable idle detection. -:: -:: Affects: Client -::============================================================================== -:: set STIGMAN_CLIENT_ADMIN_TIMEOUT= - ::============================================================================== :: STIGMAN_CLIENT_OIDC_PROVIDER :: diff --git a/api/launchers/stig-manager.sh b/api/launchers/stig-manager.sh index 4a8e050ff..9fb64ea63 100755 --- a/api/launchers/stig-manager.sh +++ b/api/launchers/stig-manager.sh @@ -79,6 +79,19 @@ #============================================================================== # export STIGMAN_CLASSIFICATION= +#============================================================================== +# STIGMAN_CLIENT_ADMIN_TIMEOUT +# +# | Default: "0" | The maximum time (in minutes) a user with admin privileges +# can be inactive in the web client before discarding their access token and +# requiring reauthorization. Activity is defined as mouse click, keypress, or +# scrolling in any tab or window of a same-origin browsing context group. Set +# to zero to disable idle detection. +# +# Affects: Client +#============================================================================== +# export STIGMAN_CLIENT_ADMIN_TIMEOUT= + #============================================================================== # STIGMAN_CLIENT_API_BASE # @@ -89,6 +102,16 @@ #============================================================================== # export STIGMAN_CLIENT_API_BASE= +#============================================================================== +# STIGMAN_CLIENT_CONSOLE_MODE +# +# | Default: "production" | The console mode of the web client, setting to +# "development" enables console logging which is otherwise disabled +# +# Affects: Client +#============================================================================== +# export STIGMAN_CLIENT_CONSOLE_MODE= + #============================================================================== # STIGMAN_CLIENT_DIRECTORY # @@ -142,19 +165,6 @@ #============================================================================== # export STIGMAN_CLIENT_ID= -#============================================================================== -# STIGMAN_CLIENT_ADMIN_TIMEOUT -# -# | Default: "0" | The maximum time (in minutes) a user with admin privileges -# can be inactive in the web client before discarding their access token and -# requiring reauthorization. Activity is defined as mouse click, keypress, or -# scrolling in any tab or window of a same-origin browsing context group. Set -# to zero to disable idle detection. -# -# Affects: Client -#============================================================================== -# export STIGMAN_CLIENT_ADMIN_TIMEOUT= - #============================================================================== # STIGMAN_CLIENT_OIDC_PROVIDER # diff --git a/api/source/bootstrap/client.js b/api/source/bootstrap/client.js index 7e3424e5b..15a576c7b 100644 --- a/api/source/bootstrap/client.js +++ b/api/source/bootstrap/client.js @@ -29,6 +29,7 @@ function getClientEnv(){ `const STIGMAN = { Env: { version: "${config.version}", + consoleMode: "${config.client.consoleMode}", apiBase: "${config.client.apiBase}", displayAppManagers: ${config.client.displayAppManagers}, stateEvents: ${config.client.stateEvents}, diff --git a/api/source/bootstrap/middlewares.js b/api/source/bootstrap/middlewares.js index 57ac9bd05..0f0c0de0c 100644 --- a/api/source/bootstrap/middlewares.js +++ b/api/source/bootstrap/middlewares.js @@ -14,6 +14,11 @@ const logger = require('../utils/logger') function configureMiddleware(app) { + // Must run before any app.use() call: Express's lazyrouter binds the query + // parser at the moment the first middleware is registered and ignores + // later changes. + app.set('query parser', 'simple') + const middlewareConfigFunctions = [ configureMulter, configureExpress, diff --git a/api/source/controllers/Asset.js b/api/source/controllers/Asset.js index ef7b43e4c..f29c31b25 100644 --- a/api/source/controllers/Asset.js +++ b/api/source/controllers/Asset.js @@ -589,8 +589,8 @@ function getCollectionIdAndVerifyAccess(request, minimumRole = Security.ROLES.Ma */ async function getAssetInfoAndVerifyAccess(request, roleId = Security.ROLES.Manage) { const assetId = request.params.assetId - const [rows] = await dbUtils.selectCollectionByAssetId(assetId) - const grant = request.userObject.grants[rows[0]?.collectionId] + const row = await dbUtils.selectCollectionByAssetId(assetId) + const grant = request.userObject.grants[row?.collectionId] // check if user has sufficient access level if (!grant || grant.roleId < roleId) { throw new SmError.PrivilegeError("Insufficient access to this asset's collection.") diff --git a/api/source/controllers/Collection.js b/api/source/controllers/Collection.js index dd7f61ce0..1ed978163 100644 --- a/api/source/controllers/Collection.js +++ b/api/source/controllers/Collection.js @@ -54,6 +54,9 @@ module.exports.createCollection = async function createCollection (req, res, nex const elevate = req.query.elevate const body = req.body if ( elevate || req.userObject.privileges.create_collection ) { + if (elevate && (body.settings !== undefined || body.labels !== undefined || body.metadata !== undefined)) { + throw new SmError.PrivilegeError('Elevated requests cannot set collection settings, labels, or metadata.') + } if (!hasUniqueGrants(body.grants)) { throw new SmError.UnprocessableError('Duplicate user or user group in grant array') } @@ -266,6 +269,9 @@ module.exports.replaceCollection = async function replaceCollection (req, res, n const projection = req.query.projection const body = req.body + if (elevate && (body.settings !== undefined || body.labels !== undefined || body.metadata !== undefined)) { + throw new SmError.PrivilegeError('Elevated requests cannot set collection settings, labels, or metadata.') + } if (!hasUniqueGrants(body.grants)) { throw new SmError.UnprocessableError('Duplicate user in grant array') } @@ -306,6 +312,9 @@ module.exports.updateCollection = async function updateCollection (req, res, nex const {collectionId, grant} = await getCollectionInfoAndCheckPermission(req, Security.ROLES.Manage, true) const projection = req.query.projection const body = req.body + if (elevate && (body.settings !== undefined || body.labels !== undefined || body.metadata !== undefined)) { + throw new SmError.PrivilegeError('Elevated requests cannot set collection settings, labels, or metadata.') + } if (body.grants) { if (!hasUniqueGrants(body.grants)) { throw new SmError.UnprocessableError('Duplicate user in grant array') diff --git a/api/source/controllers/Review.js b/api/source/controllers/Review.js index 46c9e6c32..2b2af3cb4 100644 --- a/api/source/controllers/Review.js +++ b/api/source/controllers/Review.js @@ -16,9 +16,9 @@ module.exports.postReviewsByAsset = async function postReviewsByAsset (req, res, const assetId = req.params.assetId const reviews = req.body - // check assetId exists and is enabled - const assetExists = await AssetService.doesAssetExist(assetId) - if (!assetExists) { + // check assetId exists, is enabled, and belongs to the collection in the URL path + const assetRow = await dbUtils.selectCollectionByAssetId(assetId) + if (!assetRow || assetRow.collectionId.toString() !== collectionId) { throw new SmError.PrivilegeError() } @@ -162,9 +162,10 @@ module.exports.putReviewByAssetRule = async function (req, res, next) { const {assetId, ruleId} = {...req.params} const review = {...req.body, ruleId} const projections = req.query.projection - // check assetId exists and is enabled - const assetExists = await AssetService.doesAssetExist(assetId) - if (!assetExists) { + + // check assetId exists, is enabled, and belongs to the collection in the URL path + const assetRow = await dbUtils.selectCollectionByAssetId(assetId) + if (!assetRow || assetRow.collectionId.toString() !== collectionId) { throw new SmError.PrivilegeError() } @@ -181,7 +182,7 @@ module.exports.putReviewByAssetRule = async function (req, res, next) { } const rows = await ReviewService.getReviews({ projections, - filter: {assetId, ruleId}, + filter: {collectionId, assetId, ruleId}, grant, userObject: req.userObject }) @@ -199,8 +200,17 @@ module.exports.patchReviewByAssetRule = async function (req, res, next) { } const {collectionId, grant} = await Collection.getCollectionInfoAndCheckPermission(req, Security.ROLES.Restricted) const {assetId, ruleId} = {...req.params} + + // check assetId exists, is enabled, and belongs to the collection in the URL path — + // must run before the pre-write existence read so a foreign-collection asset + // cannot satisfy the "review must exist" gate and reveal review state via 404 vs 403 + const assetRow = await dbUtils.selectCollectionByAssetId(assetId) + if (!assetRow || assetRow.collectionId.toString() !== collectionId) { + throw new SmError.PrivilegeError() + } + const currentReviews = await ReviewService.getReviews({ - filter: {assetId, ruleId}, + filter: {collectionId, assetId, ruleId}, grant, userObject: req.userObject }) @@ -222,7 +232,7 @@ module.exports.patchReviewByAssetRule = async function (req, res, next) { } const rows = await ReviewService.getReviews({ projections, - filter: {assetId, ruleId}, + filter: {collectionId, assetId, ruleId}, grant, userObject: req.userObject }) diff --git a/api/source/package-lock.json b/api/source/package-lock.json index 9820e0367..59c4cbdeb 100644 --- a/api/source/package-lock.json +++ b/api/source/package-lock.json @@ -1,12 +1,12 @@ { "name": "stig-management-api", - "version": "1.6.8", + "version": "1.6.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stig-management-api", - "version": "1.6.8", + "version": "1.6.9", "license": "MIT", "dependencies": { "ajv": "^8.17.1", @@ -19,7 +19,7 @@ "csv-stringify": "^6.5.1", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "^5.5.8", "he": "^1.2.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", @@ -35,7 +35,6 @@ "swagger-ui-express": "^4.1.6", "umzug": "^2.3.0", "undici": "^6.24.0", - "uuid": "^9.0.1", "ws": "^8.18.3", "xlsx-template": "file:utils/xlsx-template-js-zip-upgrade" } @@ -105,6 +104,18 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1329,9 +1340,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "funding": [ { "type": "github", @@ -1344,9 +1355,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz", + "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==", "funding": [ { "type": "github", @@ -1355,9 +1366,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -1718,7 +1730,6 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -2346,9 +2357,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -2918,9 +2929,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "funding": [ { "type": "github", @@ -3061,19 +3072,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/api/source/package.json b/api/source/package.json index 9616150d2..a6a4e0946 100644 --- a/api/source/package.json +++ b/api/source/package.json @@ -1,6 +1,6 @@ { "name": "stig-management-api", - "version": "1.6.8", + "version": "1.6.9", "description": "An API for managing evaluations of Security Technical Implementation Guide (STIG) assessments.", "main": "index.js", "scripts": { @@ -13,17 +13,17 @@ "license": "MIT", "private": true, "dependencies": { - "archiver": "^7.0.1", - "async-retry": "^1.3.3", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", + "archiver": "^7.0.1", + "async-retry": "^1.3.3", "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", "cors": "^2.8.5", "csv-stringify": "^6.5.1", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "^5.5.8", "he": "^1.2.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", @@ -39,7 +39,6 @@ "swagger-ui-express": "^4.1.6", "umzug": "^2.3.0", "undici": "^6.24.0", - "uuid": "^9.0.1", "ws": "^8.18.3", "xlsx-template": "file:utils/xlsx-template-js-zip-upgrade" } diff --git a/api/source/service/AssetService.js b/api/source/service/AssetService.js index 1bc673a92..b343cf3a8 100644 --- a/api/source/service/AssetService.js +++ b/api/source/service/AssetService.js @@ -1,7 +1,7 @@ 'use strict'; +const { randomUUID } = require('node:crypto') const dbUtils = require('./utils') const config = require('../utils/config') -const uuid = require('uuid') let _this = this @@ -590,7 +590,7 @@ exports.cklbFromAssetStigs = async function cklbFromAssetStigs (assetId, stigs) let revisionStrResolved // Will hold specific revision string value, as opposed to "latest" const cklb = { title: '', - id: uuid.v1(), + id: randomUUID(), active: false, mode: 1, has_path: true, @@ -738,7 +738,7 @@ exports.cklbFromAssetStigs = async function cklbFromAssetStigs (assetId, stigs) markings.push(stig.marking) } - const stigUuid = uuid.v1() + const stigUuid = randomUUID() const stigObj = { stig_name: stig.title, display_name: stig.title.replace(' Security Technical Implementation Guide', ''), @@ -755,7 +755,7 @@ exports.cklbFromAssetStigs = async function cklbFromAssetStigs (assetId, stigs) const [resultGetChecklist] = await connection.query(sqlGetChecklist, [assetId, revId]) for (const row of resultGetChecklist) { const rule = { - uuid: uuid.v1(), + uuid: randomUUID(), stig_uuid: stigUuid, target_key: null, stig_ref: null, diff --git a/api/source/service/JobService.js b/api/source/service/JobService.js index ae94a923b..8e7db014c 100644 --- a/api/source/service/JobService.js +++ b/api/source/service/JobService.js @@ -1,6 +1,6 @@ +const { randomUUID } = require('node:crypto') const dbUtils = require('./utils') const _this = this -const uuid = require('uuid') exports.queryJobs = async function ({ projections = [], filters = {} } = {}) { const columns = [ @@ -110,15 +110,22 @@ async function createEventByJob(jobId, eventData) { const params = [eventName, eventData.starts, jobId] await dbUtils.pool.query(sqlCreateEvent, params) } else if (eventData.type === 'recurring') { - let endsAt = eventData.ends ? `ENDS '${eventData.ends}'` : '' - // Interpolate the interval unit as a bare word + const intervalFields = { + minute: 'MINUTE', hour: 'HOUR', day: 'DAY', + week: 'WEEK', month: 'MONTH' + } + const intervalSql = intervalFields[eventData.interval.field] + const enabledSql = eventData.enabled === false ? 'DISABLE' : 'ENABLE' + const endsSql = eventData.ends ? 'ENDS ?' : '' const sqlCreateEvent = ` - CREATE EVENT ?? - ON SCHEDULE EVERY ? ${eventData.interval.field} STARTS ? ${endsAt} - ${eventData.enabled === false ? 'DISABLE' : 'ENABLE'} + CREATE EVENT ?? + ON SCHEDULE EVERY ? ${intervalSql} STARTS ? ${endsSql} + ${enabledSql} DO CALL run_job(?, null) ` - const params = [eventName, eventData.interval.value, eventData.starts, jobId] + const params = [eventName, eventData.interval.value, eventData.starts] + if (eventData.ends) params.push(eventData.ends) + params.push(jobId) await dbUtils.pool.query(sqlCreateEvent, params) } return eventName @@ -261,12 +268,12 @@ exports.getRunsByJob = async (jobId) => { } exports.runImmediateJob = async (jobId) => { - const v1 = uuid.v1() + const runId = randomUUID() const sql = `CREATE EVENT IF NOT EXISTS ?? ON SCHEDULE AT CURRENT_TIMESTAMP DO CALL run_job(?,?)` - await dbUtils.pool.query(sql, [`job-${jobId}-${v1}`, jobId, v1]) - return v1 + await dbUtils.pool.query(sql, [`job-${jobId}-${runId}`, jobId, runId]) + return runId } exports.getOutputByRun = async (runId, {filters}) => { diff --git a/api/source/service/MetricsService.js b/api/source/service/MetricsService.js index 3725e05f2..10a0ed4dd 100644 --- a/api/source/service/MetricsService.js +++ b/api/source/service/MetricsService.js @@ -60,8 +60,8 @@ module.exports.queryMetrics = async function ({ collectionLabelTableAlias: 'clPred' }) const innerQueryRaw = `select distinct assetId from enabled_asset left join collection_label_asset_map using (assetId) - left join collection_label clPred using(clId) where a.collectionId = ${collectionId} and ${statement}` - const innerQueryFormatted = dbUtils.pool.format(innerQueryRaw, binds ) + left join collection_label clPred using(clId) where a.collectionId = ? and ${statement}` + const innerQueryFormatted = dbUtils.pool.format(innerQueryRaw, [collectionId, ...binds]) predicates.statements.push(`a.assetId IN (${innerQueryFormatted})`) } if (filter.assetIds) { diff --git a/api/source/service/ReviewService.js b/api/source/service/ReviewService.js index f20cf3a7b..746be2b88 100644 --- a/api/source/service/ReviewService.js +++ b/api/source/service/ReviewService.js @@ -1006,8 +1006,9 @@ from ${grant.roleId === 1 ? 'inner' : 'left'} join cteAclEffective cae on sa.saId = cae.saId left join revision rev on sa.benchmarkId = rev.benchmarkId left join rev_group_rule_map rgr using (revId) -where +where a.assetId = @assetId + and a.collectionId = @collectionId and coalesce(cae.access, 'rw') = 'rw' ), cteCandidate AS ( diff --git a/api/source/service/utils.js b/api/source/service/utils.js index 421dd39f7..837a34802 100644 --- a/api/source/service/utils.js +++ b/api/source/service/utils.js @@ -344,13 +344,13 @@ module.exports.parseRevisionStr = function (revisionStr) { } module.exports.selectCollectionByAssetId = async function (assetId) { - // another possibility: return _this.pool.query(`SELECT c.* from asset a left join enabled_collection c using (collectionId) where a.assetId = ?`, [assetId]) - return _this.pool.query(`SELECT * from enabled_collection where collectionId = (select collectionId from enabled_asset where assetId = ?)`, [assetId]) + const [rows] = await _this.pool.query(`SELECT c.* from enabled_asset a left join enabled_collection c using (collectionId) where a.assetId = ?`, [assetId]) + return rows[0] } module.exports.getGrantByAssetId = async function (assetId, grants) { - const [rows] = await _this.selectCollectionByAssetId(assetId) - return rows.length ? grants[rows[0].collectionId] : null + const row = await _this.selectCollectionByAssetId(assetId) + return row ? grants[row.collectionId] : null } module.exports.getUserAssetStigAccess = async function ({assetId, benchmarkId, grants}) { diff --git a/api/source/specification/stig-manager.yaml b/api/source/specification/stig-manager.yaml index 06c04238b..2bd556b8e 100755 --- a/api/source/specification/stig-manager.yaml +++ b/api/source/specification/stig-manager.yaml @@ -2108,7 +2108,6 @@ paths: '/collections/{collectionId}/metadata': parameters: - $ref: '#/components/parameters/CollectionIdPath' - - $ref: '#/components/parameters/ElevateQuery' get: tags: - Collection @@ -2187,7 +2186,6 @@ paths: '/collections/{collectionId}/metadata/keys': parameters: - $ref: '#/components/parameters/CollectionIdPath' - - $ref: '#/components/parameters/ElevateQuery' get: tags: - Collection @@ -2213,7 +2211,6 @@ paths: - 'stig-manager:collection:read' '/collections/{collectionId}/metadata/keys/{key}': parameters: - - $ref: '#/components/parameters/ElevateQuery' - $ref: '#/components/parameters/CollectionIdPath' - $ref: '#/components/parameters/MetadataKeyPath' get: @@ -7247,6 +7244,10 @@ components: $ref: '#/components/schemas/RoleId' userId: $ref: '#/components/schemas/UserId' + labels: + type: array + items: + $ref: '#/components/schemas/LabelCreate' metadata: $ref: '#/components/schemas/Metadata' name: diff --git a/api/source/utils/config.js b/api/source/utils/config.js index e6c0a6a12..201e473d3 100644 --- a/api/source/utils/config.js +++ b/api/source/utils/config.js @@ -26,6 +26,7 @@ const config = { ? process.env.STIGMAN_CLIENT_PATH_PREFIX.replace(/\/+$/, "") + "/" : "", historyBase: process.env.STIGMAN_CLIENT_HISTORY_BASE || "", + consoleMode: process.env.STIGMAN_CLIENT_CONSOLE_MODE || "production", displayAppManagers: process.env.STIGMAN_CLIENT_DISPLAY_APPMANAGERS || "true", idleTimeoutUser: (() => { const val = parseInt(process.env.STIGMAN_CLIENT_USER_TIMEOUT) diff --git a/api/source/utils/jwksCache.js b/api/source/utils/jwksCache.js index c0f1572d3..6c142adcb 100644 --- a/api/source/utils/jwksCache.js +++ b/api/source/utils/jwksCache.js @@ -163,7 +163,7 @@ class JWKSCache extends EventEmitter { } function onTimeout() { - console.log('Request timed out', formatSocket(socketInfo)) + logger.writeError('jwksCache', 'requestTimeout', { socket: formatSocket(socketInfo) }) httpRequest.destroy() } diff --git a/api/source/utils/logSocket.js b/api/source/utils/logSocket.js index 08f79bc47..4a5442ee6 100644 --- a/api/source/utils/logSocket.js +++ b/api/source/utils/logSocket.js @@ -1,8 +1,8 @@ +const { randomUUID } = require('node:crypto') const logger = require('./logger') const WebSocket = require('ws') const component = 'logSocket' const auth = require('./auth') -const uuid = require('uuid') const SmError = require('./error') const asyncApiValidator = require('./asyncApiValidator') @@ -15,7 +15,7 @@ class LogSession { this.authorized = false; this.tokenExp = null; this.logForwarding = false; - this.sessionId = uuid.v1(); + this.sessionId = randomUUID(); this.filter = null; this.pingIntervalId = null this.unauthorizedTimerId = null diff --git a/api/source/utils/logger.js b/api/source/utils/logger.js index 4f726a609..751a35df2 100644 --- a/api/source/utils/logger.js +++ b/api/source/utils/logger.js @@ -1,5 +1,5 @@ -const uuid = require('uuid') +const { randomUUID } = require('node:crypto') const onFinished = require('on-finished') const onHeaders = require('on-headers') const config = require('./config') @@ -104,7 +104,7 @@ function requestLogger (req, res, next) { res._startAt = undefined res._startTime = undefined res.svcStatus = {} - req.requestId = uuid.v1() + req.requestId = randomUUID() // Response body length for appinfo and content for privileged requests let responseBody diff --git a/client/src/js/SM/AssetSelection.js b/client/src/js/SM/AssetSelection.js index ff1864e2f..a0d84c271 100644 --- a/client/src/js/SM/AssetSelection.js +++ b/client/src/js/SM/AssetSelection.js @@ -68,7 +68,7 @@ SM.AssetSelection.GridPanel = Ext.extend(Ext.grid.GridPanel, { let longest = Math.max(...(value.map(el => el.length))) qtipWidth = longest * 8 } - metadata.attr = ` ext:qwidth=${qtipWidth} ext:qtip="${record.data.name} STIGs
${value.join('
')}"` + metadata.attr = ` ext:qwidth=${qtipWidth} ext:qtip="${SM.he(record.data.name)} STIGs
${value.map(SM.he).join('
')}"` return `${value.length}` } }, diff --git a/client/src/js/SM/Attachments.js b/client/src/js/SM/Attachments.js index 7c58486d7..f5d128db4 100644 --- a/client/src/js/SM/Attachments.js +++ b/client/src/js/SM/Attachments.js @@ -215,8 +215,12 @@ SM.Attachments.Grid = Ext.extend(Ext.grid.GridPanel, { fpwindow.show() // could show a wait indicator for image loading if necessary try { + const allowedMimeTypes = ['image/gif', 'image/jpeg', 'image/svg+xml', 'image/png', 'image/bmp'] + if (!allowedMimeTypes.includes(artifactObj.type)) { + throw new Error(`Unsupported image type: ${artifactObj.type}`) + } const imageB64 = await getMetadataValue(artifactObj.digest) - imagePanel.update(``) + imagePanel.update(``) } catch (e) { SM.Error.handleError(e) diff --git a/client/src/js/SM/CollectionPanel.js b/client/src/js/SM/CollectionPanel.js index 096324410..284072490 100644 --- a/client/src/js/SM/CollectionPanel.js +++ b/client/src/js/SM/CollectionPanel.js @@ -199,7 +199,7 @@ SM.CollectionPanel.CommonColumns = [ renderer: function (v, md, r) { const detailedCora = r.get('coraScoreDetail') let riskClass = getRiskClass(detailedCora.riskRating) - return `
${(detailedCora.weightedAvg * 100).toFixed(1)}
` + return `
${(detailedCora.weightedAvg * 100).toFixed(1)}
` } }, { diff --git a/client/src/js/SM/Exports.js b/client/src/js/SM/Exports.js index 9b14d54b4..6f9aab05e 100644 --- a/client/src/js/SM/Exports.js +++ b/client/src/js/SM/Exports.js @@ -297,7 +297,7 @@ SM.Exports.StigTree = Ext.extend(Ext.tree.TreePanel, { const badgeClass = badgePercent === 100 ? 'sm-export-sprite-low' : badgePercent >= 50 ? 'sm-export-sprite-medium' : 'sm-export-sprite-high' let stigNode = { id: `${collectionId}-${stig.benchmarkId}-assignment-stigs-stig-node`, - text: `${stig.benchmarkId} ${badgePercent}%`, + text: `${SM.he(stig.benchmarkId)} ${badgePercent}%`, node: 'stig', collectionId: collectionId, benchmarkId: stig.benchmarkId, diff --git a/client/src/js/SM/Global.js b/client/src/js/SM/Global.js index 25e44ed6f..875ebdf11 100644 --- a/client/src/js/SM/Global.js +++ b/client/src/js/SM/Global.js @@ -252,7 +252,7 @@ SM.RuleContentTpl = new Ext.XTemplate( if (!value) return if (value.length > SM.TruncateLimit) { - return `${value.slice(0,SM.TruncateLimit)}... Full text` + return `${value.slice(0,SM.TruncateLimit)}... Full text` } else { return value diff --git a/client/src/js/SM/MainPanel.js b/client/src/js/SM/MainPanel.js index 25785e002..3fe572d6b 100644 --- a/client/src/js/SM/MainPanel.js +++ b/client/src/js/SM/MainPanel.js @@ -250,9 +250,9 @@ SM.ApplicationManagers = Ext.extend(Ext.Panel, { return `
  • - ${user.displayName} - ${user.email - ? `${user.email}` + ${SM.he(user.displayName)} + ${user.email + ? `${SM.he(user.email)}` : `No Email Available`}
  • ` diff --git a/client/src/js/SM/MetaPanel.js b/client/src/js/SM/MetaPanel.js index 0fc8d1154..702138b2c 100644 --- a/client/src/js/SM/MetaPanel.js +++ b/client/src/js/SM/MetaPanel.js @@ -76,7 +76,7 @@ SM.MetaPanel.CommonColumns = [ renderer: function (v, md, r) { const detailedCora = r.get('coraScoreDetail') let riskClass = getRiskClass(detailedCora.riskRating) - return `
    ${(detailedCora.weightedAvg * 100).toFixed(1)}
    ` + return `
    ${(detailedCora.weightedAvg * 100).toFixed(1)}
    ` } }, { diff --git a/client/src/js/SM/NavTree.js b/client/src/js/SM/NavTree.js index bd32a8ba6..f3c388f2c 100644 --- a/client/src/js/SM/NavTree.js +++ b/client/src/js/SM/NavTree.js @@ -505,7 +505,10 @@ SM.NavTree.TreePanel = Ext.extend(Ext.tree.TreePanel, { tokenParsed.exp = `${tokenParsed.exp} (${expDate.format('Y-m-d H:i:s')})` tokenParsed.iat = `${tokenParsed.iat} (${iatDate.format('Y-m-d H:i:s')})` tokenParsed.auth_time = `${tokenParsed.auth_time} (${authTimeDate.format('Y-m-d H:i:s')})` - tip.update("
    " + JSON.stringify(tokenParsed,null,2) + "
    ") + const pre = document.createElement('pre') + pre.style.whiteSpace = 'pre-wrap' + pre.textContent = JSON.stringify(tokenParsed, null, 2) + tip.body.dom.replaceChildren(pre) } } }).getId() //for sonarcloud to see object used diff --git a/client/src/js/SM/Review.js b/client/src/js/SM/Review.js index 74aeacb34..9a940f662 100644 --- a/client/src/js/SM/Review.js +++ b/client/src/js/SM/Review.js @@ -120,7 +120,7 @@ SM.Review.Form.CommentTextArea = Ext.extend(Ext.form.TextArea, { SM.Review.Form.ResultEngineSprite = Ext.extend(Ext.form.DisplayField, { generateMarkup: function(resultEngine) { if (!resultEngine) return `Manual` - const productSpan = `${resultEngine.product}` + const productSpan = `${SM.he(resultEngine.product)}` const overrideSpan = resultEngine.overrides?.length ? `Override` : '' return `${productSpan}${overrideSpan}` }, diff --git a/client/src/js/SM/ReviewsImport.js b/client/src/js/SM/ReviewsImport.js index e11e0378e..88d3f07fb 100644 --- a/client/src/js/SM/ReviewsImport.js +++ b/client/src/js/SM/ReviewsImport.js @@ -185,7 +185,7 @@ SM.ReviewsImport.Grid = Ext.extend(Ext.grid.GridPanel, { dataIndex: 'filename', sortable: true, renderer: (v, m, r) => { - m.attr = `ext:qtip="${r.data.fullPath}"` + m.attr = `ext:qtip="${SM.he(r.data.fullPath)}"` return v } }, diff --git a/client/src/js/SM/User.js b/client/src/js/SM/User.js index a39aae99b..bd8743867 100644 --- a/client/src/js/SM/User.js +++ b/client/src/js/SM/User.js @@ -931,7 +931,7 @@ SM.User.UserGrid = Ext.extend(Ext.grid.GridPanel, { renderer: function (value, metadata, record, ri, ci, store) { let qtipContent if (record.data.statusUser) { - qtipContent = `ext:qtip="Status: ${value}
    Set by: userId ${record.data.statusUser}
    Date: ${Ext.util.Format.date(record.data.statusDate,'Y-m-d H:i T')}"` + qtipContent = `ext:qtip="Status: ${SM.he(value)}
    Set by: userId ${SM.he(record.data.statusUser)}
    Date: ${Ext.util.Format.date(record.data.statusDate,'Y-m-d H:i T')}"` } metadata.attr = 'style="line-height: 17px;white-space:normal;"' return `${value}` diff --git a/client/src/js/init.js b/client/src/js/init.js index b4145b900..cdbc91e42 100644 --- a/client/src/js/init.js +++ b/client/src/js/init.js @@ -1,6 +1,12 @@ import { stylesheets, scripts, isMinimizedSource } from './resources.js' (async function () { + if (STIGMAN.Env.consoleMode !== 'development') { + console.log = function () { } + console.warn = function () { } + console.error = function () { } + console.debug = function () { } + } const statusEl = document.getElementById("loading-text") let OW // aka window.oidcWorker, created in setupOidcWorker() if (!window.isSecureContext) { diff --git a/client/src/js/jsonview.bundle.js b/client/src/js/jsonview.bundle.js index a59555d8f..450471149 100644 --- a/client/src/js/jsonview.bundle.js +++ b/client/src/js/jsonview.bundle.js @@ -21,7 +21,7 @@ var JsonView = (function (exports) { var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var key = params.key, size = params.size; - return "\n
    \n
    \n
    ".concat(key, "
    \n
    ").concat(size, "
    \n
    \n "); + return "\n
    \n
    \n
    ".concat(SM.he(key), "
    \n
    ").concat(size, "
    \n
    \n "); } function notExpandedTemplate() { @@ -29,7 +29,7 @@ var JsonView = (function (exports) { var key = params.key, value = params.value, type = params.type; - return "\n
    \n
    \n
    ".concat(key, "
    \n
    :
    \n
    ").concat(type === 'string' ? SM.he(value).replace(/\n/g,'
    ') : value, "
    \n
    \n "); + return "\n
    \n
    \n
    ".concat(SM.he(key), "
    \n
    :
    \n
    ").concat(type === 'string' ? SM.he(value).replace(/\n/g,'
    ') : value, "
    \n
    \n "); } function hideNodeChildren(node) { diff --git a/client/src/js/jszip.min.js b/client/src/js/jszip.min.js index 032de7958..ff4cfd5e8 100644 --- a/client/src/js/jszip.min.js +++ b/client/src/js/jszip.min.js @@ -1,13 +1,13 @@ /*! -JSZip v3.3.0 - A JavaScript class for generating and reading zip files +JSZip v3.10.1 - A JavaScript class for generating and reading zip files (c) 2009-2016 Stuart Knightley -Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. JSZip uses the library pako released under the MIT license : -https://github.com/nodeca/pako/blob/master/LICENSE +https://github.com/nodeca/pako/blob/main/LICENSE */ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=t()}}(function(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t>2,s=(3&e)<<4|r>>4,a=1>6:64,o=2>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))),l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){"use strict";var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/DataLengthProbe"),a=t("./stream/Crc32Probe");s=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=new n(i.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new s("data_length")),e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),t},getCompressedWorker:function(){return new n(i.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new a).pipe(new s("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new s("compressedSize")).withStreamInfo("compression",e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){"use strict";var i=t("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){"use strict";var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){"use strict";var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){"use strict";function A(t,e){var r,i="";for(r=0;r>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string",s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent||0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){"use strict";var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,e,r){"use strict";var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./DataReader":18}],21:[function(t,e,r){"use strict";var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){"use strict";var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":t=this.data.substring(this.index,e);break;case"uint8array":t=this.data.subarray(this.index,e);break;case"array":case"nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){"use strict";function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t){this.emit("error",t)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t,e,r){"use strict";var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case"blob":return h.newBlob(h.transformTo("arraybuffer",e),r);case"base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array?"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n=e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,e,a){"use strict";var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),r=0,i=e.length;r>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){"use strict";e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){"use strict";var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(u[(r=e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u=x)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0),U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f?m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)}); \ No newline at end of file +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r
    Error Fetching user data` throw(e) } - if (curUser.username !== undefined) { - loadApp(); - } else { - el.innerHTML += `
    No account for ${window.oidcWorker.token}` - } + loadApp(); } catch (e) { - el.innerHTML += `

    ` + const ta = document.createElement('textarea') + ta.className = 'sm-bootstrap-error' + ta.setAttribute('wrap', 'off') + ta.rows = 24 + ta.cols = 80 + ta.style.fontSize = '10px' + ta.readOnly = true + ta.value = JSON.stringify(STIGMAN.serializeError(e), null, 2) + el.appendChild(document.createElement('br')) + el.appendChild(document.createElement('br')) + el.appendChild(ta) } } @@ -269,7 +275,7 @@ async function loadApp () { } catch (e) { - Ext.get( 'indicator' ).dom.innerHTML = e.message + Ext.get( 'indicator' ).dom.innerHTML = SM.he(e.message) } } //end loadApp() diff --git a/client/src/js/workers/oidc-worker.js b/client/src/js/workers/oidc-worker.js index 4c22c6186..e0edba7a5 100644 --- a/client/src/js/workers/oidc-worker.js +++ b/client/src/js/workers/oidc-worker.js @@ -79,6 +79,10 @@ async function exchangeCodeForToken({ code, codeVerifier, clientId = ENV.clientI async function initialize(options) { if (!initialized) { initialized = true + const parsedRedirectUri = new URL(options.redirectUri) + if (!parsedRedirectUri.protocol.startsWith('http')) { + return { success: false, error: `Invalid redirectUri scheme: ${parsedRedirectUri.protocol}` } + } redirectUri = options.redirectUri ENV = options.env || null @@ -108,10 +112,10 @@ async function getStatus() { } function logout() { - return { - success: true, - redirect: oidcConfiguration.end_session_endpoint + if (!oidcConfiguration.end_session_endpoint) { + return { success: false, error: 'Logout not available' } } + return { success: true, redirect: oidcConfiguration.end_session_endpoint } } async function onMessage(e) { @@ -201,8 +205,20 @@ function validateOidcConfiguration() { } else if (ENV.strictPkce && !oidcConfiguration.code_challenge_methods_supported?.includes('S256')) { result.success = false result.error = 'OP does not advertise PKCE and STIGMAN_CLIENT_STRICT_PKCE=true' + } else if (oidcConfiguration.end_session_endpoint) { + try { + const parsed = new URL(oidcConfiguration.end_session_endpoint) + if (!parsed.protocol.startsWith('http')) { + console.warn(logPrefix, 'end_session_endpoint has invalid scheme, logout will be unavailable:', oidcConfiguration.end_session_endpoint) + oidcConfiguration.end_session_endpoint = null + } + } + catch { + console.warn(logPrefix, 'end_session_endpoint is not a valid URL, logout will be unavailable:', oidcConfiguration.end_session_endpoint) + oidcConfiguration.end_session_endpoint = null + } } - return result + return result } function getScopeStr() { diff --git a/docs/installation-and-setup/data-and-permissions.rst b/docs/installation-and-setup/data-and-permissions.rst index 91d94a130..c633804b9 100644 --- a/docs/installation-and-setup/data-and-permissions.rst +++ b/docs/installation-and-setup/data-and-permissions.rst @@ -174,7 +174,21 @@ STIG Manager recognizes two "privileges" that can be granted to users via config Users with the **create_collection** privilege can create new Collections of their own, but are otherwise ordinary users. -Users with the **admin** privilege must explicitly invoke the "elevate" parameter in queries to the API to make use of their privilege. In our reference UI, this parameter is sent when certain "Application Management" functions are invoked, such as importing new Reference STIGs, requesting a list of all Collections, or creating a new Grant in a Collection they do not otherwise have access to. +Users with the **admin** privilege may explicitly invoke the ``elevate`` parameter in API requests to act as a privileged principal. The elevation mechanism is designed so that an admin user does not need a separate privileged account on the identity provider — the same account is used, and the user opts into elevated mode on a per-request basis. + +When a request includes ``?elevate=true``, it is governed by the elevation access model rather than by any Collection Grant the user may also hold. Elevation is scoped exclusively to Collection management and application administration operations: + +- Enumerate, create, and delete Collections +- Read and modify a Collection's name and description +- Create, modify, and delete Grants on any Collection, assigning any Role to any User or User Group (without supplying an ACL) +- Manage Users and User Groups + +Elevation does **not** grant access to collection content. An elevated admin cannot read or write Reviews, access Asset or STIG checklist data, or modify a Collection's settings, labels, metadata, or Grant ACLs — even with ``?elevate=true`` supplied. These operations require a Collection Grant and are performed via normal (non-elevated) requests. + +In the reference UI, the ``elevate`` parameter is sent when "Application Management" functions are invoked, such as importing new Reference STIGs, listing all Collections, or creating a Grant in a Collection the admin does not otherwise have access to. + +.. note:: + An elevated admin can create a Grant giving themselves any Role in any Collection. This is intentional: it avoids requiring admins who also need content access to maintain a second OIDC account. The accepted control is that **every elevated request — including self-grant operations — has its complete request and response bodies written to the application log**, regardless of whether the request succeeds. Administrators responsible for deploying STIG Manager should ensure elevated-request log entries are retained and reviewed. These **privileges** must be present in the token presented to the API in order to be successfully invoked. diff --git a/docs/installation-and-setup/envvars.csv b/docs/installation-and-setup/envvars.csv index c6319ddaf..2bc135405 100644 --- a/docs/installation-and-setup/envvars.csv +++ b/docs/installation-and-setup/envvars.csv @@ -15,8 +15,12 @@ | If necessary, the passphrase that decrypts the PEM encoded Server private key used for TLS. Additionally requires setting ``STIGMAN_API_TLS_CERT_FILE`` to enable TLS.","API" "STIGMAN_CLASSIFICATION","| **Default** ``U`` | Sets the classification banner, if any. Available values: ``NONE`` ``U`` ``CUI`` ``C`` ``S`` ``TS`` ``SCI`` ","API, Client" +"STIGMAN_CLIENT_ADMIN_TIMEOUT","| **Default** ``0`` +| The maximum time (in minutes) a user with admin privileges can be inactive in the web client before discarding their access token and requiring reauthorization. Activity is defined as mouse click, keypress, or scrolling in any tab or window of a same-origin browsing context group. Set to zero to disable idle detection.","Client" "STIGMAN_CLIENT_API_BASE","| **Default** ``api`` | The base URL for Client requests to the API relative to ``window.location`` ","Client" +"STIGMAN_CLIENT_CONSOLE_MODE","| **Default** ``production`` +| The console mode of the web client, setting to ``development`` enables console logging which is otherwise disabled","Client" "STIGMAN_CLIENT_DIRECTORY","| **Default** ``./clients`` | The location of the web client files, relative to the API source directory. Note that if running source from a clone of the GitHub repository, the client is located at `../../clients` relative to the API directory. ","API, Client" "STIGMAN_CLIENT_DISABLED","| **Default** ``false`` @@ -27,8 +31,6 @@ | A space separated list of OAuth2 scopes to request in addition to ``stig-manager:stig`` ``stig-manager:stig:read`` ``stig-manager:collection`` ``stig-manager:user`` ``stig-manager:user:read`` ``stig-manager:op``. Some OIDC providers (Okta) generate a refresh token only if the scope ``offline_access`` is requested","Client" "STIGMAN_CLIENT_ID","| **Default** ``stig-manager`` | The OIDC clientId of the web client","Client" -"STIGMAN_CLIENT_ADMIN_TIMEOUT","| **Default** ``0`` -| The maximum time (in minutes) a user with admin privileges can be inactive in the web client before discarding their access token and requiring reauthorization. Activity is defined as mouse click, keypress, or scrolling in any tab or window of a same-origin browsing context group. Set to zero to disable idle detection.","Client" "STIGMAN_CLIENT_OIDC_PROVIDER","| **Default** Value of ``STIGMAN_OIDC_PROVIDER`` | Client override of the base URL of the OIDC provider issuing signed JWTs for the API. The string ``/.well-known/openid-configuration`` will be appended by the client when fetching metadata.","Client " "STIGMAN_CLIENT_REAUTH_ACTION","| **Default** ``popup`` diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 84b375adf..cf26c6ece 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -119,14 +119,15 @@ This is a glossary with definitions for terms like :term:`Asset`: For each Collection they are granted access to, Users can have one of 4 :term:`Roles ` , providing different capabilities and default access to your Collection. See :ref:`roles-and-access` for more information. Users can also be given one of 2 **Privileges** on the STIG Manager system. These privileges can be administered in your Authentication Provider (such as Keycloak): - * Collection Creator: Gives the User the ability to create their own Collections in STIG Manager. - * Administrator (Application Manager): Gives the user elevated access to STIG Manager via the "Application Management" node of the Nav Tree. The Administrator Privilege allows the User to: - + * Collection Creator: Gives the User the ability to create their own Collections in STIG Manager. + * Administrator (Application Manager): Gives the user the ability to invoke elevated access via the "Application Management" node of the Nav Tree. The Administrator Privilege allows the User to: + * Import new STIGs into STIG Manager, as well as Delete them. - * Create and Alter Collections, and view their metadata. - * Create and Alter Users, and view their metadata. - * Import and Export Application Data. An experimental feature that will export all the Collection data in STIG Manager - * The Administrator privilege does not by itself provide access to any Collection, however, they can Grant themselves access to any Collection in STIG Manager via the Application Manager interface. + * Enumerate, Create, and Delete Collections, and view a Collection's name and description. + * Create and modify Grants (without ACLs) on any Collection. + * Create and Alter Users and User Groups. + * Import and Export Application Data. + * The Administrator privilege does not grant access to collection content. An admin cannot read or write Reviews, access Asset or STIG checklist data, or modify a Collection's settings, labels, metadata, or Grant ACLs without holding a Collection Grant. These operations require a normal (non-elevated) request governed by a Grant. User Group A named collection of Users that can be granted access to a Collection as a single entity. User Groups can be created and modified in the User Groups interface available to Application Managers. User Groups are available to all Collection Owners and Managers for use in the Grants panel. See :ref:`roles-and-access` for more information. diff --git a/release-notes.rst b/release-notes.rst index 24fec78d0..1a8a13cd2 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,3 +1,21 @@ +1.6.9 +------- + +Changes: + + - (API) Added guard to prevent elevated requests from modifying collection ``settings``, ``labels``, or ``metadata`` on create/replace/update + - (API) Simplified asset collection retrieval in controllers + - (API) Refactored JWKS cache error logging + - (API) Replaced direct string interpolation in SQL query construction with parameterized binds in MetricsService and JobService + - (UI) New ``STIGMAN_CLIENT_CONSOLE_MODE`` environment variable to suppress console output in non-development environments + - (UI) Various escaping and DOM insertion improvements across multiple SM components + - (UI) Updated OIDC worker initialization + - (Docs) Clarified data and permissions documentation for elevated actions + - (Tests) Added regression tests for cross-collection write access; updated test utilities and collection test fixtures to align with API behavior + - (Dependencies) Update ``fast-xml-parser`` to v5.7.1 and remove the ``uuid`` runtime dependency from the API + - (Dependencies) Update ``@nuwcdivnpt/stig-manager-client-modules`` to v1.6.7 + - (Dependencies) Various security and maintenance updates + 1.6.8 ------- diff --git a/test/api/mocha/data/collection/collectionPost.test.js b/test/api/mocha/data/collection/collectionPost.test.js index 051df3de5..b93d60825 100644 --- a/test/api/mocha/data/collection/collectionPost.test.js +++ b/test/api/mocha/data/collection/collectionPost.test.js @@ -42,7 +42,7 @@ describe('POST - Collection - not all tests run for all iterations', function () it("Create a Collection and test projections",async function () { const post = JSON.parse(JSON.stringify(requestBodies.createCollection)) post.name = "testCollection" + random - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}&projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return @@ -138,7 +138,7 @@ describe('POST - Collection - not all tests run for all iterations', function () } const post = JSON.parse(JSON.stringify(requestBodies.collectionWithNoSettings)) post.name = post.name + random - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}&projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return @@ -197,7 +197,7 @@ describe('POST - Collection - not all tests run for all iterations', function () maxReviews: 10 }, } - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}&projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return @@ -221,7 +221,7 @@ describe('POST - Collection - not all tests run for all iterations', function () const post = requestBodies.createCollectionWithTestGroup let uuid = uuidv4().slice(0, 10) post.name = "testCollection" + uuid - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}&projection=grants`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return @@ -236,7 +236,7 @@ describe('POST - Collection - not all tests run for all iterations', function () const post = JSON.parse(JSON.stringify(requestBodies.createCollection)) post.grants.push(post.grants[0]) post.name = "TEST" + utils.getUUIDSubString() - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return @@ -248,7 +248,7 @@ describe('POST - Collection - not all tests run for all iterations', function () it("should throw SmError.UnprocessableError due to duplicate name exists ",async function () { const post = JSON.parse(JSON.stringify(requestBodies.createCollection)) post.name = "testCollection" + random - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=${distinct.canElevate}&projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants&projection=labels&projection=assets&projection=owners&projection=statistics&projection=stigs`, 'POST', iteration.token, post) if(distinct.canCreateCollection === false){ expect(res.status).to.eql(403) return diff --git a/test/api/mocha/data/metrics/metricsGet.test.js b/test/api/mocha/data/metrics/metricsGet.test.js index ef470524f..e72e73aa5 100644 --- a/test/api/mocha/data/metrics/metricsGet.test.js +++ b/test/api/mocha/data/metrics/metricsGet.test.js @@ -1227,5 +1227,18 @@ describe('GET - Metrics', function () { }) }) } + + describe('GET - getMetricsSummaryByCollection - large benchmarkId array', function () { + // Regression: express query parser defaults to `qs`. whose default arrayLimit (20) collapses repeated bare keys into an + // object, which the OpenAPI validator then wraps in [] and rejects with + // "request/query/benchmarkId/0 must be string". The api sets the query parser to `simple` + // and uses the built-in Node query parser which does not have this behavior + it('accepts 100 benchmarkId query values without a 400 from the validator', async function () { + const adminToken = iterations.find(i => i.name === 'stigmanadmin').token + const params = Array.from({length: 100}, (_, i) => `benchmarkId=Synthetic_Stig_${i}`).join('&') + const res = await utils.executeRequest(`${config.baseUrl}/collections/${reference.testCollection.collectionId}/metrics/summary?${params}`, 'GET', adminToken) + expect(res.status).to.not.eql(400) + }) + }) }) diff --git a/test/api/mocha/integration/collection.test.js b/test/api/mocha/integration/collection.test.js index c7dc6d3ee..916050468 100644 --- a/test/api/mocha/integration/collection.test.js +++ b/test/api/mocha/integration/collection.test.js @@ -1064,7 +1064,7 @@ describe('POST - exportToCollection - /collections/{collectionId}/export-to/{dst let exportedAssetResults let exportedAssetStatuses it('Merge provided properties with a Collection Copy', async () => { - const res = await utils.executeRequest(`${config.baseUrl}/collections/${reference.scrapCollection.collectionId}?elevate=true`, 'PATCH', user.token, { + const res = await utils.executeRequest(`${config.baseUrl}/collections/${reference.scrapCollection.collectionId}`, 'PATCH', user.token, { "metadata": { "pocName": "poc2Patched", "pocEmail": "pocEmail@email.com", @@ -1260,7 +1260,7 @@ describe('POST - exportToCollection - /collections/{collectionId}/export-to/{dst expect(res.body[0].metrics.statuses, "comparing source asset to exported asset statuses").to.eql(expectedStatuses); }) it('Merge provided properties with a Collection Copy 2', async () => { - const res = await utils.executeRequest(`${config.baseUrl}/collections/${reference.scrapCollection.collectionId}?elevate=true`, 'PATCH', user.token, { + const res = await utils.executeRequest(`${config.baseUrl}/collections/${reference.scrapCollection.collectionId}`, 'PATCH', user.token, { "metadata": { "pocName": "poc2Patched", "pocEmail": "pocEmail@email.com", @@ -1718,12 +1718,6 @@ describe('PUT - setStigAssetsByCollectionUser - /collections/{collectionId}/gran it('Add restricted user to collection Y', async () => { const res = await utils.executeRequest(`${config.baseUrl}/collections/83?elevate=true&projection=grants`, 'PATCH', user.token, { - "metadata": { - "pocName": "poc2Patched", - "pocEmail": "pocEmail@email.com", - "pocPhone": "12342", - "reqRar": "true" - }, "grants": [ { "userId": "87", diff --git a/test/api/mocha/integration/deleteHandling.test.js b/test/api/mocha/integration/deleteHandling.test.js index c9b5b2488..36567187a 100644 --- a/test/api/mocha/integration/deleteHandling.test.js +++ b/test/api/mocha/integration/deleteHandling.test.js @@ -23,7 +23,7 @@ describe('DELETE - deleteAsset - /assets/{assetId} - DELETE - deleteCollection - let deletedCollection = null it('Create a Collection in order to delete it', async () => { - const res = await utils.executeRequest(`${config.baseUrl}/collections?elevate=true&projection=grants&projection=labels`, 'POST', user.token, { + const res = await utils.executeRequest(`${config.baseUrl}/collections?projection=grants&projection=labels`, 'POST', user.token, { "name": "TEST_"+ utils.getUUIDSubString(), "description": "Collection TEST description", "settings": { diff --git a/test/api/mocha/security/reviewCrossCollectionWrite.test.js b/test/api/mocha/security/reviewCrossCollectionWrite.test.js new file mode 100644 index 000000000..89a85d235 --- /dev/null +++ b/test/api/mocha/security/reviewCrossCollectionWrite.test.js @@ -0,0 +1,460 @@ +/** + * Security Regression Tests: Unauthorized Cross-Collection Review Write (Finding 1) + * + * VULNERABILITY SUMMARY + * --------------------- + * postReviewsByAsset (POST /collections/{collectionId}/reviews/{assetId}) + * putReviewByAssetRule (PUT /collections/{collectionId}/reviews/{assetId}/{ruleId}) + * patchReviewByAssetRule (PATCH /collections/{collectionId}/reviews/{assetId}/{ruleId}) + * + * All three handlers verify that the caller holds a grant on the collectionId in the + * URL path, and that the assetId exists. None verify that the asset belongs to that + * collection. + * + * ReviewService.putReviewsByAsset builds cteGrant with: + * WHERE a.assetId = @assetId + * with no AND a.collectionId = @collectionId predicate (ReviewService.js:1000-1011). + * For non-Restricted callers (roleId > 1) the ACL join in cteGrant is a LEFT JOIN, + * so it returns rules for any asset regardless of which collection owns it. + * The write succeeds: reviews are inserted or updated in the review table for the + * victim asset using the attacker's collection's validation settings. + * + * A secondary enabler in patchReviewByAssetRule: the pre-write existence check + * (Review.js:202-206) calls getReviews with filter: {assetId, ruleId} and no + * collectionId. This allows a review in the victim collection to satisfy the + * "review must exist to be patched" gate (Review.js:207), enabling the PATCH + * write path when the asset is in a foreign collection. + * + * ATTACK SCENARIO + * --------------- + * - Collection X (collectionId: 21) — attacker's collection; attacker (lvl2) has Full grant + * - Collection Y (collectionId: 83) — victim collection; attacker has NO grant + * - Asset 153 — belongs to Collection Y; has VPN_SRG_TEST STIG mapped and an + * existing submitted review for ruleId SV-106179r1_rule + * - Attacker — user "lvl2" (userId: 21), Full grant on Collection X (21) only; + * no grant on Collection Y (83) + * + * The attacker issues POST, PUT, or PATCH to a URL using Collection X's collectionId + * but Asset 153's assetId (which belongs to Collection Y). + * + * CORRECT BEHAVIOUR (after fix) + * ------------------------------ + * After verifying the caller's grant on the URL collectionId, the API must verify + * that the assetId belongs to that collection. If it does not, the request must be + * rejected with 403 before any write occurs. + * + * HOW THESE TESTS FAIL TODAY / PASS AFTER FIX + * -------------------------------------------- + * Today: POST, PUT, and PATCH all succeed (HTTP 200/201). The write-impact tests + * verify this by reading the victim asset's review via admin token after the + * attack and confirming the review was mutated — the test asserts it was NOT + * mutated, so it fails. + * After fix: the API returns 403, no write occurs, the admin read-back confirms the + * original review is unchanged, and all assertions pass. + */ + +import { config } from '../testConfig.js' +import * as utils from '../utils/testUtils.js' +import reference from '../referenceData.js' +import { expect } from 'chai' + +// --------------------------------------------------------------------------- +// Actors +// --------------------------------------------------------------------------- + +// The attacker: Full grant (roleId 2) on Collection X (21). +// Has grants on collections 1 and 21 only — NO grant on Collection Y (83). +const attacker = { + name: 'lvl2', + userId: '21', + token: + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJleHAiOjE4NjQ3MDkwNzQsImlhdCI6MTY3MDU2ODI3NSwiYXV0aF90aW1lIjoxNjcwNTY4Mjc0LCJqdGkiOiIwM2Y0OWVmYy1jYzcxLTQ3MTItOWFjNy0xNGY5YzZiNDc1ZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvc3RpZ21hbiIsImF1ZCI6WyJyZWFsbS1tYW5hZ2VtZW50IiwiYWNjb3VudCJdLCJzdWIiOiJjMTM3ZDYzNy1mMDU2LTRjNzItOWJlZi1lYzJhZjdjMWFiYzciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzdGlnLW1hbmFnZXIiLCJub25jZSI6IjQ5MzY5ZTdmLWEyZGYtNDkxYS04YjQ0LWEwNDJjYWYyMzhlYyIsInNlc3Npb25fc3RhdGUiOiJjNmUyZTgyNi0xMzMzLTRmMDctOTc4OC03OTQxMGM5ZjJkMDYiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc3RpZ21hbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbnQiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBzdGlnLW1hbmFnZXI6Y29sbGVjdGlvbiBzdGlnLW1hbmFnZXI6c3RpZzpyZWFkIHN0aWctbWFuYWdlcjp1c2VyOnJlYWQgc3RpZy1tYW5hZ2VyOmNvbGxlY3Rpb246cmVhZCIsInNpZCI6ImM2ZTJlODI2LTEzMzMtNGYwNy05Nzg4LTc5NDEwYzlmMmQwNiIsIm5hbWUiOiJsdmwyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibHZsMiIsImdpdmVuX25hbWUiOiJsdmwyIn0.F1i8VVLNkVsaW9i83vbVyB9eFiSxX_9ZpR6K7Zs0r7pKOCMJnSOHeKIHrlMO4hW8DrbmSRrkrrXExwNtw6zUsuH8_1uxx-SVUkaQyHEMfbx1_TstkTOFcjxIWqtlVvwPIt-DlTpQ_IFuby8wDAIxUvNwogn2OoybzAy1CDMcpIA' +} + +// The restricted attacker: Restricted grant (roleId 1) on Collection X (21) via group membership +// (userId=85, belongs to userGroupId=1 which holds grantId=32, roleId=1 on collectionId=21). +// No grant on Collection Y (83). +const restrictedAttacker = { + name: 'lvl1', + userId: '85', + token: + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJleHAiOjE4NjQ3MDg5ODQsImlhdCI6MTY3MDU2ODE4NCwiYXV0aF90aW1lIjoxNjcwNTY4MTg0LCJqdGkiOiIxMDhmMDc2MC0wYmY5LTRkZjEtYjE0My05NjgzNmJmYmMzNjMiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvc3RpZ21hbiIsImF1ZCI6WyJyZWFsbS1tYW5hZ2VtZW50IiwiYWNjb3VudCJdLCJzdWIiOiJlM2FlMjdiOC1kYTIwLTRjNDItOWRmOC02MDg5ZjcwZjc2M2IiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzdGlnLW1hbmFnZXIiLCJub25jZSI6IjE0ZmE5ZDdkLTBmZTAtNDQyNi04ZmQ5LTY5ZDc0YTZmMzQ2NCIsInNlc3Npb25fc3RhdGUiOiJiNGEzYWNmMS05ZGM3LTQ1ZTEtOThmOC1kMzUzNjJhZWM0YzciLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc3RpZ21hbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbnQiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBzdGlnLW1hbmFnZXI6Y29sbGVjdGlvbiBzdGlnLW1hbmFnZXI6c3RpZzpyZWFkIHN0aWctbWFuYWdlcjp1c2VyOnJlYWQgc3RpZy1tYW5hZ2VyOmNvbGxlY3Rpb246cmVhZCIsInNpZCI6ImI0YTNhY2YxLTlkYzctNDVlMS05OGY4LWQzNTM2MmFlYzRjNyIsIm5hbWUiOiJyZXN0cmljdGVkIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibHZsMSIsImdpdmVuX25hbWUiOiJyZXN0cmljdGVkIn0.OqLARi5ILt3j2rMikXy0ECTTqjWco0-CrMwzE88gUv2i8rVO9kMgVsXbtPk2L2c9NNNujnxqg7QIr2_sqA51saTrZHvzXcsT8lBruf74OubRMwcTQqJap-COmrzb60S7512k0WfKTYlHsoCn_uAzOb9sp8Trjr0NksU8OXCElDU' +} + +// --------------------------------------------------------------------------- +// Fixture identifiers +// --------------------------------------------------------------------------- + +// Attacker's collection — attacker holds a Full grant here +const attackerCollectionId = '21' // Collection X + +// Victim collection — attacker has NO grant here +const victimCollectionId = '83' // Collection Y + +// Victim asset — belongs to Collection Y (83) +// Has VPN_SRG_TEST mapped; seed review (reviewId 13) exists for victimRuleId +// with detail "test\nvisible to lvl1" and status submitted +const victimAssetId = '153' + +// Rule present in VPN_SRG_TEST, mapped to victimAsset via stig_asset_map +const victimRuleId = 'SV-106179r1_rule' + +// The seed review detail — used to confirm the review was NOT mutated after fix +const seedDetail = 'test\nvisible to lvl1' + +// Attacker-controlled content — used to confirm mutation in the unfixed case +const attackerDetail = 'ATTACKER WROTE THIS VIA CROSS-COLLECTION WRITE' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// Read the victim review via admin token — source of truth for write-impact checks +async function getVictimReview () { + return utils.executeRequest( + `${config.baseUrl}/collections/${victimCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'GET', + config.adminToken + ) +} + +// --------------------------------------------------------------------------- + +describe('Security Regression: Unauthorized Cross-Collection Review Write (Finding 1)', () => { + + // ------------------------------------------------------------------------- + // Sanity checks — confirm the prerequisite fixture state is correct. + // These must pass both before and after the fix. + // ------------------------------------------------------------------------- + describe('Fixture sanity checks', () => { + + before(async function () { + await utils.loadAppData() + }) + + it('attacker (lvl2) has no access to victim asset 153 via GET — confirming no grant on Collection Y', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/assets/${victimAssetId}`, + 'GET', + attacker.token + ) + expect(res.status).to.equal(403) + }) + + it('attacker (lvl2) can access Collection X (21) — confirming their grant is active', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}`, + 'GET', + attacker.token + ) + expect(res.status).to.equal(200) + }) + + it('seed review exists on victim asset 153 with expected detail text', async () => { + const res = await getVictimReview() + expect(res.status).to.equal(200) + expect(res.body).to.have.property('detail', seedDetail) + }) + }) + + // ------------------------------------------------------------------------- + // POST /collections/{collectionId}/reviews/{assetId} + // + // Attack: collectionId = 21 (Collection X, attacker has Full grant) + // assetId = 153 (belongs to Collection Y, attacker has NO grant) + // + // CURRENT BEHAVIOUR (bug): HTTP 200, review written to victim asset. + // EXPECTED BEHAVIOUR (fix): HTTP 403, no write occurs. + // ------------------------------------------------------------------------- + describe('POST /collections/{collectionId}/reviews/{assetId} — cross-collection write', () => { + + beforeEach(async function () { + await utils.loadAppData() + }) + + it('SECURITY: POST must return 403 when assetId belongs to a different collection than collectionId', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}`, + 'POST', + attacker.token, + [{ ruleId: victimRuleId, result: 'pass', detail: attackerDetail, comment: 'attacker comment' }] + ) + expect(res.status, + 'Expected 403: asset 153 belongs to Collection Y (83), not Collection X (21). ' + + 'The API must reject writes that cross collection boundaries. ' + + 'If this is 200, the vulnerability is present.' + ).to.equal(403) + }) + + it('SECURITY: POST cross-collection attack must not mutate the victim review (write-impact verification)', async () => { + await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}`, + 'POST', + attacker.token, + [{ ruleId: victimRuleId, result: 'pass', detail: attackerDetail, comment: 'attacker comment' }] + ) + + // Read the victim review via admin to check whether it was actually written. + // After fix: the POST was rejected (403), so the review is unchanged. + // Today (bug): the POST succeeded, so the detail has been overwritten. + const adminRes = await getVictimReview() + expect(adminRes.status).to.equal(200) + expect(adminRes.body.detail, + 'The victim review detail must not have been modified by the cross-collection POST. ' + + `Expected the seed value "${seedDetail}" to be unchanged. ` + + 'If the detail was overwritten, the unauthorized write succeeded.' + ).to.equal(seedDetail) + }) + }) + + // ------------------------------------------------------------------------- + // PUT /collections/{collectionId}/reviews/{assetId}/{ruleId} + // + // CURRENT BEHAVIOUR (bug): HTTP 200/201, review written to victim asset. + // EXPECTED BEHAVIOUR (fix): HTTP 403, no write occurs. + // ------------------------------------------------------------------------- + describe('PUT /collections/{collectionId}/reviews/{assetId}/{ruleId} — cross-collection write', () => { + + beforeEach(async function () { + await utils.loadAppData() + }) + + it('SECURITY: PUT must return 403 when assetId belongs to a different collection than collectionId', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PUT', + attacker.token, + { result: 'pass', detail: attackerDetail, comment: 'attacker comment', status: 'saved' } + ) + expect(res.status, + 'Expected 403: asset 153 belongs to Collection Y (83), not Collection X (21). ' + + 'If this is 200, the vulnerability is present.' + ).to.equal(403) + }) + + it('SECURITY: PUT cross-collection attack must not mutate the victim review (write-impact verification)', async () => { + await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PUT', + attacker.token, + { result: 'pass', detail: attackerDetail, comment: 'attacker comment', status: 'saved' } + ) + + const adminRes = await getVictimReview() + expect(adminRes.status).to.equal(200) + expect(adminRes.body.detail, + 'The victim review detail must not have been modified by the cross-collection PUT. ' + + `Expected the seed value "${seedDetail}" to be unchanged.` + ).to.equal(seedDetail) + }) + }) + + // ------------------------------------------------------------------------- + // PATCH /collections/{collectionId}/reviews/{assetId}/{ruleId} + // + // The PATCH path has an additional enabler: the pre-write existence check + // (Review.js:202-206) calls getReviews without a collectionId filter. + // This allows the victim asset's existing review to satisfy the + // "review must exist to be patched" gate, enabling the write path. + // + // CURRENT BEHAVIOUR (bug): HTTP 200, review patched on victim asset. + // EXPECTED BEHAVIOUR (fix): HTTP 403, no write occurs. + // ------------------------------------------------------------------------- + describe('PATCH /collections/{collectionId}/reviews/{assetId}/{ruleId} — cross-collection write', () => { + + beforeEach(async function () { + await utils.loadAppData() + }) + + it('SECURITY: PATCH must return 403 when assetId belongs to a different collection than collectionId', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PATCH', + attacker.token, + { detail: attackerDetail } + ) + expect(res.status, + 'Expected 403: asset 153 belongs to Collection Y (83), not Collection X (21). ' + + 'The pre-write existence check must not satisfy itself using a review from a ' + + 'foreign collection. If this is 200, the vulnerability is present.' + ).to.equal(403) + }) + + it('SECURITY: PATCH cross-collection attack must not mutate the victim review (write-impact verification)', async () => { + await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PATCH', + attacker.token, + { detail: attackerDetail } + ) + + const adminRes = await getVictimReview() + expect(adminRes.status).to.equal(200) + expect(adminRes.body.detail, + 'The victim review detail must not have been modified by the cross-collection PATCH. ' + + `Expected the seed value "${seedDetail}" to be unchanged.` + ).to.equal(seedDetail) + }) + }) + + // ------------------------------------------------------------------------- + // Restricted-role attacker — incidentally blocked today, must remain blocked after fix. + // + // When the attacker's grant roleId === 1 (Restricted), cteGrant in + // putReviewsByAsset uses an INNER JOIN on cteAclEffective (ReviewService.js:1006). + // cteAclEffective is built from the attacker's grant IDs in Collection X. The + // victim asset's stig_asset_map entries have saId values that never appear in + // Collection X's ACL, so cteGrant returns zero rules, every incoming review gets + // error = 'no grant for this asset/ruleId', and nothing is committed. + // + // These tests confirm that the Restricted path is blocked both before and after + // the fix, and that the fix does not accidentally change this behaviour. + // The tests should PASS today and continue to PASS after the fix. + // ------------------------------------------------------------------------- + describe('Restricted-role attacker (lvl1) — blocked by ACL INNER JOIN, must stay blocked', () => { + + beforeEach(async function () { + await utils.loadAppData() + }) + + it('Restricted attacker: POST returns 403 and does not mutate victim review', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}`, + 'POST', + restrictedAttacker.token, + [{ ruleId: victimRuleId, result: 'pass', detail: attackerDetail, comment: 'restricted attacker comment' }] + ) + // The membership check (added by the Finding 1 fix) fires before putReviewsByAsset + // is called, so the Restricted user gets 403 — the same as the Full-role attacker. + expect(res.status).to.equal(403) + + const adminRes = await getVictimReview() + expect(adminRes.body.detail).to.equal(seedDetail) + }) + + it('Restricted attacker: PUT returns non-2xx and does not mutate victim review', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PUT', + restrictedAttacker.token, + { result: 'pass', detail: attackerDetail, comment: 'restricted attacker comment', status: 'saved' } + ) + expect(res.status).to.equal(403) + + const adminRes = await getVictimReview() + expect(adminRes.body.detail).to.equal(seedDetail) + }) + + it('Restricted attacker: PATCH returns 403 and does not mutate victim review', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${victimAssetId}/${victimRuleId}`, + 'PATCH', + restrictedAttacker.token, + { detail: attackerDetail } + ) + // 403 is required in all cases — not 404. + // + // Today (before fix): the PATCH pre-write read (Review.js:202-206) calls getReviews + // without collectionId. For a Restricted caller the ACL join is an INNER JOIN, so + // the victim review is invisible to that read — currentReviews is empty and the + // handler throws NotFoundError (404). This is incorrect: a 404 tells the attacker + // that no review exists for this rule on this asset from the perspective of their + // ACL, which leaks review state across a collection boundary they cannot access. + // + // After fix: the asset-collection membership check must fire BEFORE the pre-write + // existence read, so the attacker receives 403 regardless of whether a review exists + // on the victim asset. This eliminates the 403/404 oracle. + expect(res.status, + 'Expected 403 — not 404. A 404 leaks review state across a collection boundary: ' + + 'it reveals whether a review exists for this rule on this asset, which is information ' + + 'the caller has no grant to access. The collection-membership check must run before ' + + 'the pre-write existence read.' + ).to.equal(403) + + const adminRes = await getVictimReview() + expect(adminRes.body.detail).to.equal(seedDetail) + }) + }) + + // ------------------------------------------------------------------------- + // Negative controls — confirm the fix does not block legitimate same-collection writes. + // These tests must pass both before and after the fix. + // ------------------------------------------------------------------------- + describe('Negative controls — legitimate same-collection writes must still succeed', () => { + + before(async function () { + await utils.loadAppData() + }) + + // Full-role user (lvl2): asset 42 belongs to Collection X (21) + it('Full-role (lvl2): POST reviews to asset 42 in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}`, + 'POST', + attacker.token, + [{ ruleId: reference.testCollection.ruleId, result: 'pass', detail: 'legitimate post from lvl2', comment: 'comment' }] + ) + expect(res.status).to.equal(200) + }) + + it('Full-role (lvl2): PUT review to asset 42 in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}/${reference.testCollection.ruleId}`, + 'PUT', + attacker.token, + { result: 'pass', detail: 'legitimate write from lvl2', comment: 'legitimate comment', status: 'saved' } + ) + expect(res.status).to.equal(200) + expect(res.body).to.have.property('ruleId', reference.testCollection.ruleId) + expect(res.body).to.have.property('assetId', reference.testAsset.assetId) + }) + + it('Full-role (lvl2): PATCH review on asset 42 in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}/${reference.testCollection.ruleId}`, + 'PATCH', + attacker.token, + { detail: 'legitimate patch from lvl2' } + ) + expect(res.status).to.equal(200) + expect(res.body).to.have.property('ruleId', reference.testCollection.ruleId) + }) + + // Restricted-role user (lvl1): rw access via ACL rule — + // grantId=32 grants rw to label 'test-label-lvl1' (clId=2) + VPN_SRG_TEST. + // Asset 42 carries that label and has VPN_SRG_TEST mapped, so lvl1 has + // legitimate rw access to VPN_SRG_TEST rules on asset 42 within Collection X. + it('Restricted-role (lvl1): POST review to ACL-granted asset 42 / VPN_SRG_TEST in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}`, + 'POST', + restrictedAttacker.token, + [{ ruleId: reference.testCollection.ruleId, result: 'pass', detail: 'legitimate post from lvl1', comment: 'comment' }] + ) + expect(res.status).to.equal(200) + }) + + it('Restricted-role (lvl1): PUT review to ACL-granted asset 42 / VPN_SRG_TEST in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}/${reference.testCollection.ruleId}`, + 'PUT', + restrictedAttacker.token, + { result: 'pass', detail: 'legitimate write from lvl1', comment: 'legitimate comment', status: 'saved' } + ) + expect(res.status).to.equal(200) + expect(res.body).to.have.property('ruleId', reference.testCollection.ruleId) + expect(res.body).to.have.property('assetId', reference.testAsset.assetId) + }) + + it('Restricted-role (lvl1): PATCH review on ACL-granted asset 42 / VPN_SRG_TEST in Collection X succeeds', async () => { + const res = await utils.executeRequest( + `${config.baseUrl}/collections/${attackerCollectionId}/reviews/${reference.testAsset.assetId}/${reference.testCollection.ruleId}`, + 'PATCH', + restrictedAttacker.token, + { detail: 'legitimate patch from lvl1' } + ) + expect(res.status).to.equal(200) + expect(res.body).to.have.property('ruleId', reference.testCollection.ruleId) + }) + }) +}) diff --git a/test/api/mocha/utils/testUtils.js b/test/api/mocha/utils/testUtils.js index d8ea7e26a..4c584d67d 100644 --- a/test/api/mocha/utils/testUtils.js +++ b/test/api/mocha/utils/testUtils.js @@ -193,7 +193,7 @@ const createTempCollection = async (collectionPost) => { } } - const res = await fetch(`${baseUrl}/collections?elevate=true&projection=grants&projection=labels`, { + const res = await fetch(`${baseUrl}/collections?projection=grants&projection=labels`, { method: 'POST', headers: { Authorization: `Bearer ${adminToken}`, diff --git a/test/api/package-lock.json b/test/api/package-lock.json index 7f3eeccb1..22f20c971 100644 --- a/test/api/package-lock.json +++ b/test/api/package-lock.json @@ -9,14 +9,14 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@nuwcdivnpt/stig-manager-client-modules": "^1.6.6", + "@nuwcdivnpt/stig-manager-client-modules": "^1.6.7", "chai": "^5.1.2", "chai-datetime": "^1.8.1", "deep-equal-in-any-order": "^2.0.6", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "5.7.0", "jszip": "^3.10.1", "mocha": "^11.7.5", - "uuid": "^11.0.3", + "uuid": "^14.0.0", "ws": "^8.18.3" }, "devDependencies": { @@ -112,10 +112,22 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nuwcdivnpt/stig-manager-client-modules": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@nuwcdivnpt/stig-manager-client-modules/-/stig-manager-client-modules-1.6.6.tgz", - "integrity": "sha512-gqiHLeblktGkJLEnUxBhcfWLeKQ23jFaua+hIMPLINf4MwOIzECshgBlHB8IcK+YqphiK5wuplNS/2glPWLeNw==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@nuwcdivnpt/stig-manager-client-modules/-/stig-manager-client-modules-1.6.7.tgz", + "integrity": "sha512-g1/wm/sjod+B61NIQz7u0E85TwWMOm+e0aWxDa4PIbmUlU29j8yQvrh9xVCkoMnenz/hCmK9AJUh8pRAs5Jj3g==", "license": "MIT" }, "node_modules/@pkgjs/parseargs": { @@ -400,9 +412,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "funding": [ { "type": "github", @@ -415,9 +427,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.0.tgz", + "integrity": "sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==", "funding": [ { "type": "github", @@ -426,9 +438,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -869,15 +882,6 @@ "marge": "bin/cli.js" } }, - "node_modules/mochawesome/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -948,9 +952,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -1188,9 +1192,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "funding": [ { "type": "github", @@ -1240,15 +1244,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/validator": { diff --git a/test/api/package.json b/test/api/package.json index 85b5954a5..c4a8c119a 100644 --- a/test/api/package.json +++ b/test/api/package.json @@ -11,19 +11,20 @@ "license": "ISC", "type": "module", "dependencies": { - "@nuwcdivnpt/stig-manager-client-modules": "^1.6.6", + "@nuwcdivnpt/stig-manager-client-modules": "^1.6.7", "chai": "^5.1.2", "chai-datetime": "^1.8.1", "deep-equal-in-any-order": "^2.0.6", - "fast-xml-parser": "5.5.8", + "fast-xml-parser": "5.7.0", "jszip": "^3.10.1", "mocha": "^11.7.5", - "uuid": "^11.0.3", + "uuid": "^14.0.0", "ws": "^8.18.3" }, "overrides": { "serialize-javascript": "^7.0.5", - "diff": "^8.0.3" + "diff": "^8.0.3", + "uuid": "^14.0.0" }, "devDependencies": { "mochawesome": "^7.1.3" diff --git a/test/state/mocha/bootstrap.test.js b/test/state/mocha/bootstrap.test.js index 4170dbeb7..9325ac365 100644 --- a/test/state/mocha/bootstrap.test.js +++ b/test/state/mocha/bootstrap.test.js @@ -21,8 +21,9 @@ describe('Boot with no dependencies', function () { }) after(async function () { - await api.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('GET /op/state', function () { @@ -113,10 +114,11 @@ describe('Boot with both dependencies', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('GET /op/state', function () { @@ -127,7 +129,7 @@ describe('Boot with both dependencies', function () { expect(res.body.dependencies).to.eql({db: true, oidc: true}) }) }) - + describe('GET /op/configuration', function () { it('should return 200 when dependencies are available', async function () { const res = await simpleRequest(`${apiOrigin}/api/op/configuration`) @@ -189,10 +191,11 @@ describe('Boot with old mysql', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('exit code', function () { @@ -249,10 +252,11 @@ describe('Boot with insecure kid - allow insecure tokens false', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('exit code', function () { @@ -312,10 +316,11 @@ describe('Boot without insecure kid - request with insecure token' , function () }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('GET /op/state', function () { @@ -361,10 +366,11 @@ describe('Boot with STIGMAN_JWKS_CACHE_MAX_AGE out of range', function () { }) after(async function () { - await mysql.stop() - await oidc.stop() - await api.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) await api.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('Mimimum value enforced', function () { @@ -382,7 +388,8 @@ describe('Boot with STIGMAN_JWKS_CACHE_MAX_AGE out of range', function () { }) }) after(async function () { - await api.stop() + this.timeout(60000) + if (api) await api.stop().catch(() => {}) }) it('should return minimum oauth.maxCacheAge (1)', async function () { const configLog = api.logRecords.filter(r => r.type === 'configuration')[0] @@ -405,7 +412,8 @@ describe('Boot with STIGMAN_JWKS_CACHE_MAX_AGE out of range', function () { }) }) after(async function () { - await api.stop() + this.timeout(60000) + if (api) await api.stop().catch(() => {}) }) it('should return maximum oauth.maxCacheAge (35791)', async function () { const configLog = api.logRecords.filter(r => r.type === 'configuration')[0] @@ -428,7 +436,8 @@ describe('Boot with STIGMAN_JWKS_CACHE_MAX_AGE out of range', function () { }) }) after(async function () { - await api.stop() + this.timeout(60000) + if (api) await api.stop().catch(() => {}) }) it('should return default oauth.maxCacheAge (10)', async function () { const configLog = api.logRecords.filter(r => r.type === 'configuration')[0] diff --git a/test/state/mocha/db.test.js b/test/state/mocha/db.test.js index a611c439a..ce4be4bab 100644 --- a/test/state/mocha/db.test.js +++ b/test/state/mocha/db.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { getPorts, spawnApiPromise, spawnMySQL, simpleRequest, execIpTables } from './lib.js' +import { getPorts, spawnApiPromise, spawnMySQL, simpleRequest, execIpTables, waitForLog } from './lib.js' import MockOidc from '../../utils/mockOidc.js' import addContext from 'mochawesome/addContext.js' @@ -10,16 +10,7 @@ describe('DB outage: shutdown', function () { let mysql let oidc - async function waitLogEvent(type, count = 1) { - let seen = 0 - return new Promise((resolve) => { - api.logEvents.on(type, function (log) { - seen++ - if (seen >= count) resolve(log) - }) - }) - } - + before(async function () { this.timeout(60000) oidc = new MockOidc({keyCount: 1, includeInsecureKid: false}) @@ -47,10 +38,11 @@ describe('DB outage: shutdown', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('DB up', function () { @@ -64,8 +56,10 @@ describe('DB outage: shutdown', function () { }) describe('DB shutdown', function () { + let logMark before(async function () { this.timeout(30000) + logMark = api.logRecords.length await mysql.stop() console.log(' mysql shutdown') }) @@ -74,21 +68,23 @@ describe('DB outage: shutdown', function () { const res = await simpleRequest(`${apiOrigin}/api/op/state`) expect(res.status).to.equal(200) expect(res.body.currentState).to.equal('unavailable') - expect(res.body.dependencies).to.eql({db: false, oidc: true}) + expect(res.body.dependencies).to.eql({db: false, oidc: true}) }) it('should log retry fail', async function () { this.timeout(30000) console.log(' wait for log: restore (2)') - const log = await waitLogEvent('restore', 2) + const log = await waitForLog(api, 'restore', {count: 2, since: logMark}) expect(log.data.message).to.equal(`connect ECONNREFUSED 127.0.0.1:${dbPort}`) }) }) describe('DB restarted', function() { + let logMark before( async function() { this.timeout(30000) console.log(' try mysql restart') + logMark = api.logRecords.length mysql = await spawnMySQL({tag: '8.0.24', port: dbPort}) console.log(' ✔ mysql restarted') }) @@ -96,7 +92,7 @@ describe('DB outage: shutdown', function () { it('should return state "available"', async function () { this.timeout(60000) console.log(' wait for log: state-changed') - const log = await waitLogEvent('state-changed') + const log = await waitForLog(api, 'state-changed', {since: logMark}) expect(log.data.currentState).to.equal('available') expect(log.data.previousState).to.equal('unavailable') const res = await simpleRequest(`${apiOrigin}/api/op/state`) @@ -112,16 +108,6 @@ describe('DB outage: network/host down', function () { let mysql let oidc - async function waitLogEvent(type, count = 1) { - let seen = 0 - return new Promise((resolve) => { - api.logEvents.on(type, function (log) { - seen++ - if (seen >= count) resolve(log) - }) - }) - } - before(async function () { this.timeout(60000) oidc = new MockOidc({keyCount: 1, includeInsecureKid: false}) @@ -149,10 +135,11 @@ describe('DB outage: network/host down', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('Network/host up', function () { @@ -166,33 +153,37 @@ describe('DB outage: network/host down', function () { }) describe('Network/host down', function () { + let logMark before(async function () { + logMark = api.logRecords.length execIpTables(`-A OUTPUT -p tcp --dport ${dbPort} -j DROP`) console.log(' iptables dropping packets') }) it('should return state "unavailable"', async function () { this.timeout(30000) console.log(' wait for log: state-changed') - const log = await waitLogEvent('state-changed') + const log = await waitForLog(api, 'state-changed', {since: logMark}) expect(log.data.currentState).to.equal('unavailable') expect(log.data.previousState).to.equal('available') const res = await simpleRequest(`${apiOrigin}/api/op/state`) expect(res.status).to.equal(200) expect(res.body.currentState).to.equal('unavailable') - expect(res.body.dependencies).to.eql({db: false, oidc: true}) + expect(res.body.dependencies).to.eql({db: false, oidc: true}) }) it('should log retry fail', async function () { this.timeout(45000) console.log(' wait for log: restore (2)') - const log = await waitLogEvent('restore', 2) + const log = await waitForLog(api, 'restore', {count: 2, since: logMark}) expect(log.data.message).to.equal('connect ETIMEDOUT') }) }) describe('Network/host up', function() { + let logMark before( async function() { this.timeout(30000) + logMark = api.logRecords.length execIpTables(`-D OUTPUT -p tcp --dport ${dbPort} -j DROP`) console.log(' iptables accepting packets') }) @@ -200,7 +191,7 @@ describe('DB outage: network/host down', function () { it('should return state "available"', async function () { this.timeout(60000) console.log(' wait for log: state-changed') - const log = await waitLogEvent('state-changed') + const log = await waitForLog(api, 'state-changed', {since: logMark}) expect(log.data.currentState).to.equal('available') expect(log.data.previousState).to.equal('unavailable') const res = await simpleRequest(`${apiOrigin}/api/op/state`) diff --git a/test/state/mocha/jwks.test.js b/test/state/mocha/jwks.test.js index 113abb8e2..c429348d1 100644 --- a/test/state/mocha/jwks.test.js +++ b/test/state/mocha/jwks.test.js @@ -15,7 +15,7 @@ describe('JWKS Tests', function () { const {apiPort, dbPort, oidcPort, apiOrigin, oidcOrigin} = getPorts(54020) before(async function () { - this.timeout(30000) + this.timeout(60000) oidc = new MockOidc({keyCount: 1, includeInsecureKid: false}) tokens.rotation0 = oidc.getToken({username: 'prerotation', privileges:['create_collection']}) // default privileges oidc.rotateKeys({keyCount: 1, includeInsecureKid: false}) @@ -46,10 +46,11 @@ describe('JWKS Tests', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('Create user according to token', function () { diff --git a/test/state/mocha/lib.js b/test/state/mocha/lib.js index 73302ecb4..6baceedc3 100644 --- a/test/state/mocha/lib.js +++ b/test/state/mocha/lib.js @@ -113,6 +113,35 @@ export function spawnApi ({ } } +/** + * Resolves with a log record matching `type` once `count` such records have + * been seen at index >= `since` in `api.logRecords`. Race-safe: counts past + * records so the helper still resolves if the trigger fired before the caller + * awaited. Capture `api.logRecords.length` into `since` *before* triggering + * the action that produces the event(s). + * @param {Object} api - Result of spawnApiPromise; must have logRecords + logEvents. + * @param {string} type - The log record type to wait for. + * @param {Object} [opts] + * @param {number} [opts.count=1] - Number of matching records before resolving. + * @param {number} [opts.since=0] - Index in api.logRecords at which to start counting. + * @param {(log: Object) => boolean} [opts.predicate] - Optional filter applied to each record. + * @returns {Promise} Resolves with the count-th matching log record. + */ +export function waitForLog (api, type, { count = 1, since = 0, predicate = null } = {}) { + const past = api.logRecords.slice(since).filter(r => + r.type === type && (!predicate || predicate(r)) + ) + if (past.length >= count) return Promise.resolve(past[count - 1]) + let seen = past.length + return new Promise((resolve) => { + api.logEvents.on(type, (log) => { + if (predicate && !predicate(log)) return + seen++ + if (seen >= count) resolve(log) + }) + }) +} + /** * Waits for a child process to close. * @param {ChildProcess} child - The child process to wait for. diff --git a/test/state/mocha/oidc.test.js b/test/state/mocha/oidc.test.js index 9d65bead2..15b2dafca 100644 --- a/test/state/mocha/oidc.test.js +++ b/test/state/mocha/oidc.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { getPorts, spawnApiPromise, spawnMySQL, simpleRequest } from './lib.js' +import { getPorts, spawnApiPromise, spawnMySQL, simpleRequest, waitForLog } from './lib.js' import MockOidc from '../../utils/mockOidc.js' import addContext from 'mochawesome/addContext.js' @@ -11,17 +11,7 @@ describe('OIDC state', function () { let cachedKid const {apiPort, dbPort, oidcPort, apiOrigin, oidcOrigin} = getPorts(54030) - - async function waitLogType(type, count = 1) { - let seen = 0 - return new Promise((resolve) => { - api.logEvents.on(type, function (log) { - seen++ - if (seen >= count) resolve(log) - }) - }) - } - + before(async function () { this.timeout(60000) oidc = new MockOidc({keyCount: 1, includeInsecureKid: false}) @@ -49,17 +39,18 @@ describe('OIDC state', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) describe('OIDC up', function () { it('should log cacheUpdate with 1 kid', async function () { this.timeout(20000) console.log(' wait for log: jwksCacheEvent/cacheUpdate') - const log = await waitLogType('jwksCacheEvent') + const log = await waitForLog(api, 'jwksCacheEvent') expect(log.data.event).to.equal('cacheUpdate') const kids = Object.keys(log.data.kids) cachedKid = kids[0] @@ -67,7 +58,7 @@ describe('OIDC state', function () { }) it('should return state "available"', async function () { this.timeout(20000) - await waitLogType('started') + await waitForLog(api, 'started') const res = await simpleRequest(`${apiOrigin}/api/op/state`) expect(res.status).to.equal(200) expect(res.body.currentState).to.equal('available') @@ -76,40 +67,44 @@ describe('OIDC state', function () { }) describe('OIDC down', function () { + let logMark before(async function () { + logMark = api.logRecords.length await oidc.stop() console.log(' oidc shutdown') }) it('should log cache update attempt', async function () { this.timeout(45000) console.log(' wait for log: refreshing cache') - const log = await waitLogType('refreshing cache') + const log = await waitForLog(api, 'refreshing cache', {since: logMark}) expect(log.data.uri).to.equal(`${oidcOrigin}/jwks`) }) it('should log refresh error', async function () { this.timeout(15000) console.log(' wait for log: refresh error') - const log = await waitLogType('refresh error') + const log = await waitForLog(api, 'refresh error', {since: logMark}) expect(log.data.message).to.equal('updateCache returned false') }) it('should return state "unavailable"', async function () { this.timeout(75000) console.log(' wait for log: state-changed') - const log = await waitLogType('state-changed') + const log = await waitForLog(api, 'state-changed', {since: logMark}) expect(log.data.currentState).to.equal('unavailable') expect(log.data.previousState).to.equal('available') }) }) describe('OIDC restarted', function () { + let logMark before(async function () { + logMark = api.logRecords.length await oidc.start({port: oidcPort}) console.log(' ✔ oidc started') }) it('should log cacheUpdate with same kid as bootstrap', async function () { this.timeout(20000) console.log(' wait for log: jwksCacheEvent/cacheUpdate') - const log = await waitLogType('jwksCacheEvent') + const log = await waitForLog(api, 'jwksCacheEvent', {since: logMark}) expect(log.data.event).to.equal('cacheUpdate') const kids = Object.keys(log.data.kids) expect(kids).to.have.lengthOf(1) @@ -120,19 +115,21 @@ describe('OIDC state', function () { const res = await simpleRequest(`${apiOrigin}/api/op/state`) expect(res.status).to.equal(200) expect(res.body.currentState).to.equal('available') - expect(res.body.dependencies).to.eql({db: true, oidc: true}) + expect(res.body.dependencies).to.eql({db: true, oidc: true}) }) }) describe('OIDC rekeyed', function () { + let logMark before(async function () { + logMark = api.logRecords.length await oidc.rotateKeys({keyCount: 1, includeInsecureKid: false}) console.log(' ✔ oidc rekeyed') }) it('should log cacheUpdate with different kid than bootstrap', async function () { this.timeout(40000) console.log(' wait for log: jwksCacheEvent/cacheUpdate') - const log = await waitLogType('jwksCacheEvent') + const log = await waitForLog(api, 'jwksCacheEvent', {since: logMark}) expect(log.data.event).to.equal('cacheUpdate') const kids = Object.keys(log.data.kids) expect(kids).to.have.lengthOf(1) diff --git a/test/state/mocha/tokenValidation.test.js b/test/state/mocha/tokenValidation.test.js index ce3e9899f..141a8a6b1 100644 --- a/test/state/mocha/tokenValidation.test.js +++ b/test/state/mocha/tokenValidation.test.js @@ -41,10 +41,11 @@ describe('Token validation', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) it('should accept token having correct audience (string)', async function () { @@ -139,10 +140,11 @@ describe('Token validation', function () { }) after(async function () { - await api.stop() - await mysql.stop() - await oidc.stop() - addContext(this, {title: 'api-log', value: api.logRecords}) + this.timeout(60000) + if (api) await api.stop().catch(() => {}) + if (mysql) await mysql.stop().catch(() => {}) + if (oidc) await oidc.stop().catch(() => {}) + if (api) addContext(this, {title: 'api-log', value: api.logRecords}) }) it('should accept top-level scope "stig-manager"', async function () { diff --git a/test/state/package-lock.json b/test/state/package-lock.json index c53c0be5c..9e2f49289 100644 --- a/test/state/package-lock.json +++ b/test/state/package-lock.json @@ -1391,12 +1391,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/validator": { diff --git a/test/state/package.json b/test/state/package.json index b0adf3355..32a3ac873 100644 --- a/test/state/package.json +++ b/test/state/package.json @@ -5,7 +5,8 @@ }, "overrides": { "serialize-javascript": "^7.0.5", - "diff": "^8.0.3" + "diff": "^8.0.3", + "uuid": "^14.0.0" }, "dependencies": { "chai": "^5.2.0", diff --git a/test/unit/package-lock.json b/test/unit/package-lock.json index 7085f9a4d..b3b1ccac3 100644 --- a/test/unit/package-lock.json +++ b/test/unit/package-lock.json @@ -1132,12 +1132,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/which": { diff --git a/test/unit/package.json b/test/unit/package.json index 777660b28..e0126738f 100644 --- a/test/unit/package.json +++ b/test/unit/package.json @@ -5,7 +5,8 @@ }, "overrides": { "serialize-javascript": "^7.0.5", - "diff": "^8.0.3" + "diff": "^8.0.3", + "uuid": "^14.0.0" }, "dependencies": { "chai": "^5.2.0", diff --git a/test/utils/mockOidc.js b/test/utils/mockOidc.js index 4a3fc363d..f0d979d68 100644 --- a/test/utils/mockOidc.js +++ b/test/utils/mockOidc.js @@ -724,20 +724,16 @@ class MockOidc { } stop () { + // Order matters: drop active connections first, then await close() so the + // returned promise doesn't resolve until the listening socket has been + // released. Resolving early lets a follow-up start() on the same port + // race with kernel teardown and intermittently throw EADDRINUSE. return new Promise((resolve, reject) => { - if (this.server) { - this.server.close((err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - this.server.closeAllConnections() - this.server = null - } else { - resolve() - } + if (!this.server) return resolve() + const server = this.server + this.server = null + server.closeAllConnections() + server.close((err) => err ? reject(err) : resolve()) }) } }