diff --git a/.github/workflows/e2e-ui-tests.yml b/.github/workflows/e2e-ui-tests.yml index 6dc8130cc0..3f773875fc 100644 --- a/.github/workflows/e2e-ui-tests.yml +++ b/.github/workflows/e2e-ui-tests.yml @@ -26,6 +26,15 @@ jobs: image: rroemhild/test-openldap ports: - 10389:10389 + mocksaml: + image: boxyhq/mock-saml + ports: + - 4000:4000 + env: + APP_URL: http://localhost:4000 + ENTITY_ID: http://localhost:4000/heimdall-local + PUBLIC_KEY: ${{ secrets.MOCKSAML_PUBLIC_KEY }} + PRIVATE_KEY: ${{ secrets.MOCKSAML_PRIVATE_KEY }} splunk: image: splunk/splunk volumes: diff --git a/apps/backend/README.md b/apps/backend/README.md index d677338a76..0b175346e9 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -1,5 +1,7 @@ # Heimdall Backend +Logos used by this project are sourced from SVG Repo and are covered by the Logo License or MIT License: https://www.svgrepo.com/page/licensing/ + Create the database by setting the appropriate environment variables found in `.env-example` in `.env` Run the following to create, migrate, and seed the database: diff --git a/apps/backend/package.json b/apps/backend/package.json index 54af24de0a..38de68f8d5 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -39,6 +39,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/sequelize": "^11.0.0", "@nestjs/serve-static": "^5.0.3", + "@node-saml/passport-saml": "^5.1.0", "@types/connect-pg-simple": "^7.0.0", "@types/express": "^5.0.0", "@types/express-session": "^1.17.3", diff --git a/apps/backend/src/authn/authn.controller.ts b/apps/backend/src/authn/authn.controller.ts index 20b75ec6a0..67a7fc6778 100644 --- a/apps/backend/src/authn/authn.controller.ts +++ b/apps/backend/src/authn/authn.controller.ts @@ -172,6 +172,27 @@ export class AuthnController { await this.setSessionCookies(req, session); } + @Get('saml') + @UseFilters(new AuthenticationExceptionFilter()) + @UseGuards(AuthGuard('saml')) + async loginToSAML( + @Req() req: Request, + ): Promise<{ accessToken: string; userID: string }> { + this.logger.debug('in the saml login func'); + this.logger.debug(JSON.stringify(req.session, null, 2)); + return this.authnService.login(req.user as User); + } + + @Post('saml/callback') + @UseFilters(new AuthenticationExceptionFilter()) + @UseGuards(AuthGuard('saml')) + async getUserFromSAML(@Req() req: Request): Promise { + this.logger.debug('in the saml login callback func'); + this.logger.debug(JSON.stringify(req.session, null, 2)); + const session = await this.authnService.login(req.user as User); + await this.setSessionCookies(req, session); + } + async setSessionCookies( req: Request, session: { @@ -187,4 +208,4 @@ export class AuthnController { }); req.res?.redirect('/'); } -} +} \ No newline at end of file diff --git a/apps/backend/src/authn/authn.module.ts b/apps/backend/src/authn/authn.module.ts index a1a1280e2a..0f71afd47a 100644 --- a/apps/backend/src/authn/authn.module.ts +++ b/apps/backend/src/authn/authn.module.ts @@ -20,6 +20,7 @@ import {LDAPStrategy} from './ldap.strategy'; import {LocalStrategy} from './local.strategy'; import {OidcStrategy} from './oidc.strategy'; import {OktaStrategy} from './okta.strategy'; +import { SAMLStrategy } from './saml.strategy'; async function buildHttpsProxyAgent(proxyUrl: string): Promise { const {HttpsProxyAgent} = await import('https-proxy-agent'); @@ -45,6 +46,7 @@ async function buildHttpsProxyAgent(proxyUrl: string): Promise { GoogleStrategy, LDAPStrategy, ApiKeyService, + SAMLStrategy, { provide: OidcStrategy, useFactory: async ( @@ -77,4 +79,4 @@ async function buildHttpsProxyAgent(proxyUrl: string): Promise { ], controllers: [AuthnController] }) -export class AuthnModule {} +export class AuthnModule {} \ No newline at end of file diff --git a/apps/backend/src/authn/authn.service.ts b/apps/backend/src/authn/authn.service.ts index db2e9f3117..7a6bcd1056 100644 --- a/apps/backend/src/authn/authn.service.ts +++ b/apps/backend/src/authn/authn.service.ts @@ -1,3 +1,4 @@ +import type {AuthStrategy} from '@heimdall/common/interfaces'; import { ForbiddenException, Injectable, @@ -100,7 +101,7 @@ export class AuthnService { email: string, firstName: string, lastName: string, - creationMethod: string + creationMethod: AuthStrategy ): Promise { let user: User; try { diff --git a/apps/backend/src/authn/oidc.strategy.ts b/apps/backend/src/authn/oidc.strategy.ts index a56eaf1708..163054e151 100644 --- a/apps/backend/src/authn/oidc.strategy.ts +++ b/apps/backend/src/authn/oidc.strategy.ts @@ -140,4 +140,4 @@ export class OidcStrategy extends PassportStrategy(Strategy as any, 'oidc') { ) ); } -} +} \ No newline at end of file diff --git a/apps/backend/src/authn/resolve_claim.ts b/apps/backend/src/authn/resolve_claim.ts new file mode 100644 index 0000000000..3e24d6c9c1 --- /dev/null +++ b/apps/backend/src/authn/resolve_claim.ts @@ -0,0 +1,21 @@ +import { UnauthorizedException } from '@nestjs/common'; +import _ from 'lodash'; + +export function getRequiredClaim( + claims: Record, + defaultClaimName: string, + configuredClaimName?: string, +): string { + const resolvedClaimName = configuredClaimName || defaultClaimName; + const claimValue = _.get(claims, [resolvedClaimName]) + ?? _.get(claims, resolvedClaimName); + const firstClaimValue = Array.isArray(claimValue) + ? claimValue.find(value => typeof value === 'string' && value.length > 0) + : claimValue; + + if (typeof firstClaimValue === 'string' && firstClaimValue.length > 0) { + return firstClaimValue; + } + + throw new UnauthorizedException(`Missing required claim "${resolvedClaimName}".`); +} diff --git a/apps/backend/src/authn/saml.strategy.ts b/apps/backend/src/authn/saml.strategy.ts new file mode 100644 index 0000000000..e094e99d56 --- /dev/null +++ b/apps/backend/src/authn/saml.strategy.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import type { Profile, SamlScopingConfig } from '@node-saml/passport-saml'; +import { Strategy } from '@node-saml/passport-saml'; +import winston from 'winston'; +import { ConfigService } from '../config/config.service'; +import { User } from '../users/user.model'; +import { AuthnService } from './authn.service'; +import { getRequiredClaim } from './resolve_claim'; + +function getSamlScoping( + configService: ConfigService, +): SamlScopingConfig | undefined { + const idpList = configService.get('SAML_SCOPING_IDP_LIST'); + const proxyCount = configService.get('SAML_SCOPING_PROXY_COUNT'); + const requesterId = configService.get('SAML_SCOPING_REQUESTER_ID'); + + if (!proxyCount && !requesterId && !idpList) { + return undefined; + } + + return { + idpList: idpList + ? JSON.parse(idpList) + : undefined, + proxyCount: proxyCount + ? Number(proxyCount) + : undefined, + requesterId: requesterId?.includes(',') + ? requesterId.split(',').map(value => value.trim()) + : requesterId, + }; +} + +@Injectable() +export class SAMLStrategy extends PassportStrategy(Strategy as any, 'saml') { + public loggingTimeFormat = 'MMM-DD-YYYY HH:mm:ss Z'; + private readonly line = '_______________________________________________\n'; + public logger = winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp({ format: this.loggingTimeFormat }), + winston.format.printf( + info => + `${this.line}[${[info.timestamp]}] (Authn Service): ${info.message}`, + ), + ), + transports: [new winston.transports.Console()], + }); + + constructor( + private readonly authnService: AuthnService, + private readonly configService: ConfigService, + ) { + const samlAudience = configService.get('SAML_AUDIENCE'); + super({ + acceptedClockSkewMs: Number(configService.get('SAML_ACCEPTED_CLOCK_SKEW_MS') ?? 0), + additionalAuthorizeParams: configService.get('SAML_ADDITIONAL_AUTHORIZE_PARAMS') ? JSON.parse(configService.get('SAML_ADDITIONAL_AUTHORIZE_PARAMS')!) : undefined, + additionalLogoutParams: configService.get('SAML_ADDITIONAL_LOGOUT_PARAMS') ? JSON.parse(configService.get('SAML_ADDITIONAL_LOGOUT_PARAMS')!) : undefined, + additionalParams: configService.get('SAML_ADDITIONAL_PARAMS') ? JSON.parse(configService.get('SAML_ADDITIONAL_PARAMS')!) : undefined, + allowCreate: (configService.get('SAML_ALLOW_CREATE') ?? 'true').toLowerCase() === 'true', + attributeConsumingServiceIndex: configService.get('SAML_ATTRIBUTE_CONSUMING_SERVICE_INDEX'), + audience: + samlAudience?.toLowerCase() === 'false' + ? false + : samlAudience + || configService.get('SAML_ISSUER'), + authnContext: configService.get('SAML_AUTHN_CONTEXT') ? configService.get('SAML_AUTHN_CONTEXT')?.split(',').map(value => value.trim()) : undefined, + authnRequestBinding: configService.get('SAML_AUTHN_REQUEST_BINDING'), + callbackUrl: + configService.get('SAML_CALLBACK_URL') + || `${configService.getExternalUrl()}/authn/saml/callback`, + decryptionPvk: configService.get('SAML_DECRYPTION_PVK'), + digestAlgorithm: configService.get('SAML_DIGEST_ALGORITHM'), + disableRequestAcsUrl: (configService.get('SAML_DISABLE_REQUEST_ACS_URL') ?? '').toLowerCase() === 'true', + disableRequestedAuthnContext: (configService.get('SAML_DISABLE_REQUESTED_AUTHN_CONTEXT') ?? '').toLowerCase() === 'true', + entryPoint: configService.get('SAML_ENTRY_POINT') || 'disabled', + forceAuthn: (configService.get('SAML_FORCE_AUTHN') ?? '').toLowerCase() === 'true', + identifierFormat: configService.get('SAML_IDENTIFIER_FORMAT'), + idpCert: configService.get('SAML_IDP_CERT') || 'disabled', + idpIssuer: configService.get('SAML_IDP_ISSUER'), + issuer: configService.get('SAML_ISSUER') || 'disabled', + logoutCallbackUrl: configService.get('SAML_LOGOUT_CALLBACK_URL'), + logoutUrl: configService.get('SAML_LOGOUT_URL'), + maxAssertionAgeMs: Number(configService.get('SAML_MAX_ASSERTION_AGE_MS') ?? 0), + metadataContactPerson: configService.get('SAML_METADATA_CONTACT_PERSON') ? JSON.parse(configService.get('SAML_METADATA_CONTACT_PERSON')!) : undefined, + metadataOrganization: configService.get('SAML_METADATA_ORGANIZATION') ? JSON.parse(configService.get('SAML_METADATA_ORGANIZATION')!) : undefined, + passive: (configService.get('SAML_PASSIVE') ?? '').toLowerCase() === 'true', + privateKey: configService.get('SAML_PRIVATE_KEY'), + providerName: configService.get('SAML_PROVIDER_NAME'), + publicCert: configService.get('SAML_PUBLIC_CERT'), + racComparison: configService.get('SAML_RAC_COMPARISON') as + | 'better' + | 'exact' + | 'maximum' + | 'minimum' + | undefined, + requestIdExpirationPeriodMs: Number(configService.get('SAML_REQUEST_ID_EXPIRATION_PERIOD_MS') ?? 28_800_000), + samlAuthnRequestExtensions: configService.get('SAML_AUTHN_REQUEST_EXTENSIONS') ? JSON.parse(configService.get('SAML_AUTHN_REQUEST_EXTENSIONS')!) : undefined, + samlLogoutRequestExtensions: configService.get('SAML_LOGOUT_REQUEST_EXTENSIONS') ? JSON.parse(configService.get('SAML_LOGOUT_REQUEST_EXTENSIONS')!) : undefined, + scoping: getSamlScoping(configService), + signatureAlgorithm: configService.get('SAML_SIGNATURE_ALGORITHM') as 'sha1' | 'sha256' | 'sha512' | undefined, + signMetadata: (configService.get('SAML_SIGN_METADATA') ?? '').toLowerCase() === 'true', + skipRequestCompression: (configService.get('SAML_SKIP_REQUEST_COMPRESSION') ?? '').toLowerCase() === 'true', + spNameQualifier: configService.get('SAML_SP_NAME_QUALIFIER'), + validateInResponseTo: configService.get('SAML_VALIDATE_IN_RESPONSE_TO') as 'always' | 'ifPresent' | 'never' | undefined, + wantAssertionsSigned: (configService.get('SAML_WANT_ASSERTIONS_SIGNED') ?? 'true').toLowerCase() === 'true', + wantAuthnResponseSigned: (configService.get('SAML_WANT_AUTHN_RESPONSE_SIGNED') ?? 'true').toLowerCase() === 'true', + xmlSignatureTransforms: configService.get('SAML_XML_SIGNATURE_TRANSFORMS') ? JSON.parse(configService.get('SAML_XML_SIGNATURE_TRANSFORMS')!) : undefined, + }); + } + + async validate(profile: Profile): Promise { + this.logger.debug('in saml strategy file'); + this.logger.debug(JSON.stringify(profile, null, 2)); + return this.authnService.validateOrCreateUser( + getRequiredClaim( + profile, + 'email', + this.configService.get('SAML_EMAIL_ATTRIBUTE'), + ), + getRequiredClaim( + profile, + 'firstName', + this.configService.get('SAML_FIRST_NAME_ATTRIBUTE'), + ), + getRequiredClaim( + profile, + 'lastName', + this.configService.get('SAML_LAST_NAME_ATTRIBUTE'), + ), + 'saml', + ); + } +} diff --git a/apps/backend/src/config/config.service.ts b/apps/backend/src/config/config.service.ts index b56c39f48a..e006ff8984 100644 --- a/apps/backend/src/config/config.service.ts +++ b/apps/backend/src/config/config.service.ts @@ -1,6 +1,16 @@ -import {SequelizeOptions} from 'sequelize-typescript'; +import { AUTH_STRATEGY } from '@heimdall/common/interfaces'; +import type { AuthStrategy } from '@heimdall/common/interfaces'; +import type { SequelizeOptions } from 'sequelize-typescript'; import AppConfig from '../../config/app_config'; -import {StartupSettingsDto} from './dto/startup-settings.dto'; +import { StartupSettingsDto } from './dto/startup-settings.dto'; + +const OAUTH_AUTH_STRATEGIES = [ + AUTH_STRATEGY.GITHUB, + AUTH_STRATEGY.GITLAB, + AUTH_STRATEGY.GOOGLE, + AUTH_STRATEGY.OKTA, + AUTH_STRATEGY.OIDC, +] as const; export class ConfigService { private readonly appConfig: AppConfig; @@ -34,14 +44,28 @@ export class ConfigService { return this.get('NODE_ENV')?.toLowerCase() === 'production'; } - enabledOauthStrategies() { - const enabledOauth: string[] = []; - supportedOauth.forEach((oauthStrategy) => { - if (this.get(`${oauthStrategy.toUpperCase()}_CLIENTID`)) { - enabledOauth.push(oauthStrategy); - } - }); - return enabledOauth; + enabledAuthStrategies(): AuthStrategy[] { + const enabledAuthStrategies: AuthStrategy[] = []; + if (this.isLocalLoginAllowed()) { + enabledAuthStrategies.push(AUTH_STRATEGY.LOCAL); + } + if (this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true') { + enabledAuthStrategies.push(AUTH_STRATEGY.LDAP); + } + enabledAuthStrategies.push( + ...OAUTH_AUTH_STRATEGIES.filter(authStrategy => + this.get(`${authStrategy.toUpperCase()}_CLIENTID`), + ), + ); + if ( + ['SAML_ENTRY_POINT', 'SAML_ISSUER', 'SAML_IDP_CERT'].every(setting => + this.get(setting), + ) + ) { + enabledAuthStrategies.push(AUTH_STRATEGY.SAML); + } + + return enabledAuthStrategies; } frontendStartupSettings(): StartupSettingsDto { @@ -53,12 +77,10 @@ export class ConfigService { classificationBannerText: this.get('CLASSIFICATION_BANNER_TEXT') || '', classificationBannerTextColor: this.get('CLASSIFICATION_BANNER_TEXT_COLOR') || 'white', - enabledOAuth: this.enabledOauthStrategies(), + enabledAuthStrategies: this.enabledAuthStrategies(), externalUrl: this.getExternalUrl(), oidcName: this.get('OIDC_NAME') || '', - ldap: this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true' || false, registrationEnabled: this.isRegistrationAllowed(), - localLoginEnabled: this.isLocalLoginAllowed(), tenableHostUrl: this.getTenableHostUrl(), forceTenableFrontend: this.get('FORCE_TENABLE_FRONTEND')?.toLowerCase() === 'true', @@ -94,10 +116,3 @@ export class ConfigService { return this.appConfig.get(key); } } -export const supportedOauth: string[] = [ - 'github', - 'gitlab', - 'google', - 'okta', - 'oidc' -]; diff --git a/apps/backend/src/config/dto/startup-settings.dto.ts b/apps/backend/src/config/dto/startup-settings.dto.ts index 3af275b85b..9522770b1f 100644 --- a/apps/backend/src/config/dto/startup-settings.dto.ts +++ b/apps/backend/src/config/dto/startup-settings.dto.ts @@ -1,4 +1,4 @@ -import {IStartupSettings} from '@heimdall/common/interfaces'; +import type {AuthStrategy, IStartupSettings} from '@heimdall/common/interfaces'; export class StartupSettingsDto implements IStartupSettings { readonly apiKeysEnabled: boolean; @@ -6,15 +6,13 @@ export class StartupSettingsDto implements IStartupSettings { readonly classificationBannerColor: string; readonly classificationBannerText: string; readonly classificationBannerTextColor: string; - readonly enabledOAuth: string[]; + readonly enabledAuthStrategies: AuthStrategy[]; readonly externalUrl: string; + readonly forceTenableFrontend: boolean; readonly oidcName: string; - readonly ldap: boolean; readonly registrationEnabled: boolean; - readonly localLoginEnabled: boolean; - readonly tenableHostUrl: string; - readonly forceTenableFrontend: boolean; readonly splunkHostUrl: string; + readonly tenableHostUrl: string; constructor(settings: IStartupSettings) { this.apiKeysEnabled = settings.apiKeysEnabled; @@ -22,12 +20,10 @@ export class StartupSettingsDto implements IStartupSettings { this.classificationBannerColor = settings.classificationBannerColor; this.classificationBannerText = settings.classificationBannerText; this.classificationBannerTextColor = settings.classificationBannerTextColor; - this.enabledOAuth = settings.enabledOAuth; + this.enabledAuthStrategies = settings.enabledAuthStrategies; this.externalUrl = settings.externalUrl; this.oidcName = settings.oidcName; - this.ldap = settings.ldap; this.registrationEnabled = settings.registrationEnabled; - this.localLoginEnabled = settings.localLoginEnabled; this.tenableHostUrl = settings.tenableHostUrl; this.forceTenableFrontend = settings.forceTenableFrontend; this.splunkHostUrl = settings.splunkHostUrl; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 54768d316b..6d65208b7a 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -69,7 +69,9 @@ async function bootstrap() { // Sessions was previously set to only be used for oauth callbacks // but now is used for Tenable authentication as well. if ( - configService.enabledOauthStrategies().length || + configService + .enabledAuthStrategies() + .some((strategy) => strategy !== 'local') || configService.getTenableHostUrl().length ) { app.use( diff --git a/apps/backend/src/users/dto/create-user.dto.ts b/apps/backend/src/users/dto/create-user.dto.ts index 9a75a0d463..a4e6bf677d 100644 --- a/apps/backend/src/users/dto/create-user.dto.ts +++ b/apps/backend/src/users/dto/create-user.dto.ts @@ -1,18 +1,16 @@ -import {ICreateUser} from '@heimdall/common/interfaces'; -import {IsEmail, IsIn, IsNotEmpty, IsOptional, IsString} from 'class-validator'; +import { AUTH_STRATEGIES } from '@heimdall/common/interfaces'; +import type { AuthStrategy, ICreateUser } from '@heimdall/common/interfaces'; +import { IsEmail, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateUserDto implements ICreateUser { - @IsEmail() - @IsNotEmpty() - readonly email!: string; - + @IsIn(AUTH_STRATEGIES) @IsNotEmpty() @IsString() - readonly password!: string; + readonly creationMethod!: AuthStrategy; + @IsEmail() @IsNotEmpty() - @IsString() - readonly passwordConfirmation!: string; + readonly email!: string; @IsOptional() @IsString() @@ -26,17 +24,20 @@ export class CreateUserDto implements ICreateUser { @IsString() readonly organization: string | undefined; - @IsOptional() + @IsNotEmpty() @IsString() - readonly title: string | undefined; + readonly password!: string; @IsNotEmpty() @IsString() + readonly passwordConfirmation!: string; + @IsIn(['user']) + @IsNotEmpty() + @IsString() readonly role!: string; - @IsNotEmpty() + @IsOptional() @IsString() - @IsIn(['local', 'ldap', 'github', 'gitlab', 'google', 'okta', 'ldap']) - readonly creationMethod!: string; + readonly title: string | undefined; } diff --git a/apps/backend/test/.env-ci b/apps/backend/test/.env-ci index 31a2147a75..f45720b94c 100644 --- a/apps/backend/test/.env-ci +++ b/apps/backend/test/.env-ci @@ -27,4 +27,8 @@ OIDC_USER_INFO_URL=http://localhost:8082/userinfo OIDC_CLIENTID=test OIDC_CLIENT_SECRET=test +SAML_ENTRY_POINT=http://localhost:4000/api/saml/sso +SAML_ISSUER=heimdall-local +SAML_IDP_CERT=MIIDGTCCAgGgAwIBAgIUSJFig5zdo7VMWbXz+4OU5OdJX3QwDQYJKoZIhvcNAQELBQAwHDEaMBgGA1UEAwwRaGVpbWRhbGwtbW9ja3NhbWwwHhcNMjYwNzA2MTgzNjU1WhcNMzYwNzAzMTgzNjU1WjAcMRowGAYDVQQDDBFoZWltZGFsbC1tb2Nrc2FtbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJRvvo61xktZi7h4mqOxSz+b64SXZ6qh2/fZAmym3JPr0HN+3e8l5ZOprJMMXxDG2MZOpAJ87eRn+vxS63jU5x1Ao9M/Mjx7cG+QEnTlbj0NY5kztt5TiHSkIOCTR/ExcszTKmxH6plPvQHWzJ9uwFBudbZ4GQlx9s7QrJpJKFiTFfE7QImAKyH1pSsiPHVkMxUjhQlbO8mIvacjfHxxAuJ98ff7vB8SZOXOWEig24JyJ+WJNDecUtRO0MyOaEf69ZmXWxFHoUzV7RxNbRp3lS7liMc13Wtlcf7oZ2HjPY+497IHbI7tKauKp3Kaln98r6WIw4SZw378kZdB8C+JQ0CAwEAAaNTMFEwHQYDVR0OBBYEFNl53BVUE9eAU379BCKlvwEPQ2nCMB8GA1UdIwQYMBaAFNl53BVUE9eAU379BCKlvwEPQ2nCMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACX1VWW3495NZnkNMpA6ruAg99mpMB389mhlKigegEvRaPs6NAC0RI3COCBFCROcwm9DSsuOgyWWr+CHcUl7AnUUwBB4wsiaAg37Q2oJWHYW+wHzAHynzTXo9HCqPmxy4lh4i6b0ZivrPqEPCeB2gOUKxsdjaMxitJMRYHi0cQKQfHn7QxXWja0F1a5yirKxRHNUPo5Bkh0o2rfX5SRh5scQqb2eNcUp9/qzyATOz3cHB3QRdCY2IL3nfTN0VMWIyeHn7+nYZuB9ksRyEBbVZa47H4tI9sFm2weDDE24CVt4sJndVkFBkLLWl419ojMu89g7pQQleYeNiuSvx9zQhn4= + SPLUNK_HOST_URL=https://localhost:8089 diff --git a/apps/frontend/src/assets/github.svg b/apps/frontend/src/assets/github.svg new file mode 100644 index 0000000000..6f0683a7fe --- /dev/null +++ b/apps/frontend/src/assets/github.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/frontend/src/assets/github_mark.png b/apps/frontend/src/assets/github_mark.png deleted file mode 100644 index 628da97c70..0000000000 Binary files a/apps/frontend/src/assets/github_mark.png and /dev/null differ diff --git a/apps/frontend/src/assets/gitlab.svg b/apps/frontend/src/assets/gitlab.svg new file mode 100644 index 0000000000..71a666d049 --- /dev/null +++ b/apps/frontend/src/assets/gitlab.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/frontend/src/assets/gitlab_mark.png b/apps/frontend/src/assets/gitlab_mark.png deleted file mode 100644 index 3b554826a0..0000000000 Binary files a/apps/frontend/src/assets/gitlab_mark.png and /dev/null differ diff --git a/apps/frontend/src/assets/google.svg b/apps/frontend/src/assets/google.svg new file mode 100644 index 0000000000..71fa93f6bc --- /dev/null +++ b/apps/frontend/src/assets/google.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/frontend/src/assets/google_mark.png b/apps/frontend/src/assets/google_mark.png deleted file mode 100644 index 40f81be906..0000000000 Binary files a/apps/frontend/src/assets/google_mark.png and /dev/null differ diff --git a/apps/frontend/src/assets/okta.svg b/apps/frontend/src/assets/okta.svg new file mode 100644 index 0000000000..e14836ff51 --- /dev/null +++ b/apps/frontend/src/assets/okta.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/frontend/src/assets/okta_mark.png b/apps/frontend/src/assets/okta_mark.png deleted file mode 100644 index 02f1f1f6df..0000000000 Binary files a/apps/frontend/src/assets/okta_mark.png and /dev/null differ diff --git a/apps/frontend/src/assets/openid.svg b/apps/frontend/src/assets/openid.svg new file mode 100644 index 0000000000..4eec98752f --- /dev/null +++ b/apps/frontend/src/assets/openid.svg @@ -0,0 +1,2 @@ + +OpenID icon \ No newline at end of file diff --git a/apps/frontend/src/assets/openid_mark.png b/apps/frontend/src/assets/openid_mark.png deleted file mode 100644 index 72ad05fa75..0000000000 Binary files a/apps/frontend/src/assets/openid_mark.png and /dev/null differ diff --git a/apps/frontend/src/assets/saml.svg b/apps/frontend/src/assets/saml.svg new file mode 100644 index 0000000000..cec79efb0c --- /dev/null +++ b/apps/frontend/src/assets/saml.svg @@ -0,0 +1,14 @@ + + + + certificate-check-solid + + + + + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/components/global/RegistrationModal.vue b/apps/frontend/src/components/global/RegistrationModal.vue index 30e7453743..bd6c0b793f 100644 --- a/apps/frontend/src/components/global/RegistrationModal.vue +++ b/apps/frontend/src/components/global/RegistrationModal.vue @@ -137,6 +137,7 @@ import Modal from '@/components/global/Modal.vue'; import UserValidatorMixin from '@/mixins/UserValidatorMixin'; import {ServerModule} from '@/store/server'; import {SnackbarModule} from '@/store/snackbar'; +import type {AuthStrategy} from '@heimdall/common/interfaces'; import { validatePasswordBoolean, validators @@ -153,7 +154,7 @@ export interface SignupHash { password: string; passwordConfirmation: string; role: string; - creationMethod: string; + creationMethod: AuthStrategy; } @Component({ diff --git a/apps/frontend/src/components/global/UserModal.vue b/apps/frontend/src/components/global/UserModal.vue index 09f74a1f2e..edc9845166 100644 --- a/apps/frontend/src/components/global/UserModal.vue +++ b/apps/frontend/src/components/global/UserModal.vue @@ -515,10 +515,7 @@ export default class UserModal extends Vue { } get update_unavailable() { - return ( - this.userInfo.creationMethod === 'ldap' || - ServerModule.enabledOAuth.includes(this.userInfo.creationMethod) - ); + return this.userInfo.creationMethod !== 'local'; } get title(): string { diff --git a/apps/frontend/src/components/global/login/LocalLogin.vue b/apps/frontend/src/components/global/login/LocalLogin.vue index c9e14fc22b..1803139b17 100644 --- a/apps/frontend/src/components/global/login/LocalLogin.vue +++ b/apps/frontend/src/components/global/login/LocalLogin.vue @@ -34,7 +34,7 @@ > @@ -57,9 +57,9 @@ NOTE: This Heimdall instance is in an external-authentication only mode. - mdi-help-circle-outline + + mdi-help-circle-outline + @@ -84,12 +84,12 @@ id="oauth-oidc" class="mt-5 flex-fill" plain - @click="oauthLogin('oidc')" + @click="startExternalLogin('oidc')" >
Login with {{ oidcName }}
@@ -98,12 +98,12 @@ id="oauth-google" class="mt-5 flex-fill" plain - @click="oauthLogin('google')" + @click="startExternalLogin('google')" >
Login with Google
@@ -112,12 +112,12 @@ id="oauth-github" class="mt-5 flex-fill" plain - @click="oauthLogin('github')" + @click="startExternalLogin('github')" >
Login with GitHub
Login with GitLab
@@ -139,15 +139,29 @@ id="oauth-okta" class="mt-5 flex-fill" plain - @click="oauthLogin('okta')" + @click="startExternalLogin('okta')" >
Login with Okta
+ + +
Login with SAML
+
@@ -155,79 +169,83 @@ diff --git a/apps/frontend/src/store/server.ts b/apps/frontend/src/store/server.ts index dc88d396a1..aad79854cc 100644 --- a/apps/frontend/src/store/server.ts +++ b/apps/frontend/src/store/server.ts @@ -1,6 +1,7 @@ import Store from '@/store/store'; import {LocalStorageVal} from '@/utilities/helper_util'; -import { +import type { + AuthStrategy, ISlimUser, IStartupSettings, IUpdateUser, @@ -29,12 +30,10 @@ export interface IServerState { classificationBannerColor: string; classificationBannerText: string; classificationBannerTextColor: string; - enabledOAuth: string[]; + enabledAuthStrategies: AuthStrategy[]; externalUrl: string; registrationEnabled: boolean; oidcName: string; - ldap: boolean; - localLoginEnabled: boolean; userInfo: IUser; tenableHostUrl: string; forceTenableFrontend: boolean; @@ -58,13 +57,11 @@ class Server extends VuexModule implements IServerState { classificationBannerColor = ''; classificationBannerText = ''; classificationBannerTextColor = ''; - ldap = false; + enabledAuthStrategies: AuthStrategy[] = ['local']; serverUrl = ''; serverMode = false; registrationEnabled = true; - localLoginEnabled = true; loading = true; - enabledOAuth: string[] = []; externalUrl: string = ''; allUsers: ISlimUser[] = []; oidcName = ''; @@ -109,12 +106,10 @@ class Server extends VuexModule implements IServerState { this.classificationBannerText = settings.classificationBannerText; this.classificationBannerColor = settings.classificationBannerColor; this.classificationBannerTextColor = settings.classificationBannerTextColor; - this.enabledOAuth = settings.enabledOAuth; + this.enabledAuthStrategies = settings.enabledAuthStrategies; this.externalUrl = settings.externalUrl; this.registrationEnabled = settings.registrationEnabled; this.oidcName = settings.oidcName; - this.ldap = settings.ldap; - this.localLoginEnabled = settings.localLoginEnabled; this.tenableHostUrl = settings.tenableHostUrl; this.forceTenableFrontend = settings.forceTenableFrontend; this.splunkHostUrl = settings.splunkHostUrl; @@ -236,7 +231,7 @@ class Server extends VuexModule implements IServerState { email: string; password: string; passwordConfirmation: string; - creationMethod: string; + creationMethod: AuthStrategy; }) { return axios.post('/users', userInfo); } diff --git a/apps/frontend/src/views/Login.vue b/apps/frontend/src/views/Login.vue index fffaa7e8f7..b580294553 100644 --- a/apps/frontend/src/views/Login.vue +++ b/apps/frontend/src/views/Login.vue @@ -103,16 +103,15 @@ export default class Login extends Vue { get anyAuthProvidersAvailable() { return ( - ServerModule.localLoginEnabled || ServerModule.enabledOAuth.length !== 0 + ServerModule.enabledAuthStrategies.includes('local') || + ServerModule.enabledAuthStrategies.some( + (strategy) => strategy !== 'local' && strategy !== 'ldap' + ) ); } get ldapenabled() { - return ServerModule.ldap; - } - - get localLoginEnabled() { - return ServerModule.localLoginEnabled; + return ServerModule.enabledAuthStrategies.includes('ldap'); } get logoffFailure() { diff --git a/libs/common/interfaces/config/auth-strategy.interface.ts b/libs/common/interfaces/config/auth-strategy.interface.ts new file mode 100644 index 0000000000..c56d99b481 --- /dev/null +++ b/libs/common/interfaces/config/auth-strategy.interface.ts @@ -0,0 +1,28 @@ +export const AUTH_STRATEGY = { + GITHUB: 'github', + GITLAB: 'gitlab', + GOOGLE: 'google', + LDAP: 'ldap', + LOCAL: 'local', + OIDC: 'oidc', + OKTA: 'okta', + SAML: 'saml', +} as const; + +export const AUTH_STRATEGIES = [ + AUTH_STRATEGY.LOCAL, + AUTH_STRATEGY.LDAP, + AUTH_STRATEGY.GITHUB, + AUTH_STRATEGY.GITLAB, + AUTH_STRATEGY.GOOGLE, + AUTH_STRATEGY.OKTA, + AUTH_STRATEGY.OIDC, + AUTH_STRATEGY.SAML, +] as const; + +export type AuthStrategy = (typeof AUTH_STRATEGIES)[number]; + +export type ExternalAuthStrategy = Exclude< + AuthStrategy, + typeof AUTH_STRATEGY.LOCAL +>; diff --git a/libs/common/interfaces/config/startup-settings.interface.ts b/libs/common/interfaces/config/startup-settings.interface.ts index f52b032bc3..d3f583981d 100644 --- a/libs/common/interfaces/config/startup-settings.interface.ts +++ b/libs/common/interfaces/config/startup-settings.interface.ts @@ -1,16 +1,16 @@ -export interface IStartupSettings { +import type { AuthStrategy } from './auth-strategy.interface'; + +export type IStartupSettings = { readonly apiKeysEnabled: boolean; readonly banner: string; readonly classificationBannerColor: string; readonly classificationBannerText: string; readonly classificationBannerTextColor: string; - readonly enabledOAuth: string[]; + readonly enabledAuthStrategies: AuthStrategy[]; readonly externalUrl: string; + readonly forceTenableFrontend: boolean; readonly oidcName: string; - readonly ldap: boolean; readonly registrationEnabled: boolean; - readonly localLoginEnabled: boolean; - readonly tenableHostUrl: string; - readonly forceTenableFrontend: boolean; readonly splunkHostUrl: string; -} + readonly tenableHostUrl: string; +}; diff --git a/libs/common/interfaces/index.js b/libs/common/interfaces/index.js new file mode 100644 index 0000000000..94a9c570d9 --- /dev/null +++ b/libs/common/interfaces/index.js @@ -0,0 +1,24 @@ +const AUTH_STRATEGY = { + GITHUB: 'github', + GITLAB: 'gitlab', + GOOGLE: 'google', + LDAP: 'ldap', + LOCAL: 'local', + OIDC: 'oidc', + OKTA: 'okta', + SAML: 'saml', +}; + +const AUTH_STRATEGIES = [ + AUTH_STRATEGY.LOCAL, + AUTH_STRATEGY.LDAP, + AUTH_STRATEGY.GITHUB, + AUTH_STRATEGY.GITLAB, + AUTH_STRATEGY.GOOGLE, + AUTH_STRATEGY.OKTA, + AUTH_STRATEGY.OIDC, + AUTH_STRATEGY.SAML, +]; + +exports.AUTH_STRATEGIES = AUTH_STRATEGIES; +exports.AUTH_STRATEGY = AUTH_STRATEGY; diff --git a/libs/common/interfaces/index.ts b/libs/common/interfaces/index.ts index b7fb118700..f8e3e2fcb9 100644 --- a/libs/common/interfaces/index.ts +++ b/libs/common/interfaces/index.ts @@ -3,6 +3,14 @@ export * from './apikey/create-apikey.interface'; export * from './apikey/delete-apikey.interface'; export * from './apikey/regenerate-apikey.interface'; export * from './apikey/update-apikey.interface'; +export { + AUTH_STRATEGIES, + AUTH_STRATEGY, +} from './config/auth-strategy.interface'; +export type { + AuthStrategy, + ExternalAuthStrategy, +} from './config/auth-strategy.interface'; export * from './config/startup-settings.interface'; export * from './evaluation-tag/create-evaluation-tag.interface'; export * from './evaluation-tag/delete-evaluation-tag.interface'; diff --git a/libs/common/interfaces/user/create-user.interface.ts b/libs/common/interfaces/user/create-user.interface.ts index b87750dee9..1d2cf5e8e8 100644 --- a/libs/common/interfaces/user/create-user.interface.ts +++ b/libs/common/interfaces/user/create-user.interface.ts @@ -1,10 +1,13 @@ -export interface ICreateUser { +import type { AuthStrategy } from '../config/auth-strategy.interface'; + +export type ICreateUser = { + readonly creationMethod: AuthStrategy; readonly email: string; - readonly password: string; - readonly passwordConfirmation: string; readonly firstName: string | undefined; readonly lastName: string | undefined; readonly organization: string | undefined; - readonly title: string | undefined; + readonly password: string; + readonly passwordConfirmation: string; readonly role: string; -} + readonly title: string | undefined; +}; diff --git a/test/integration/login.cy.ts b/test/integration/login.cy.ts index 52ecb6b719..255e0c736f 100644 --- a/test/integration/login.cy.ts +++ b/test/integration/login.cy.ts @@ -53,6 +53,14 @@ context('Login', () => { // Make sure all the fields exist userModalVerifier.verifyFieldsExist(); }); + it('authenticates a saml user', () => { + loginPage.loginSaml('saml'); + dropdown.openUserModal(); + userModalVerifier.verifyFieldsExist(); + cy.get('#email_field').should('have.value', 'saml@example.com'); + cy.get('#firstName').should('have.value', 'saml'); + cy.get('#lastName').should('have.value', 'saml'); + }); describe('Authentication Failures', () => { // Ignore 401 errors on authentication failure tests diff --git a/test/support/pages/LoginPage.ts b/test/support/pages/LoginPage.ts index 3e05bd0c7a..af2ecc15d4 100644 --- a/test/support/pages/LoginPage.ts +++ b/test/support/pages/LoginPage.ts @@ -11,6 +11,19 @@ export default class LoginPage { cy.get('[data-cy=ldapLoginButton]').click(); } + loginSaml(username: string): void { + const origin = String( + Cypress.env('MOCKSAML_ORIGIN') || 'http://localhost:4000', + ); + + cy.get('#saml').click({ force: true }); + cy.origin(origin, { args: { username } }, ({ username }) => { + cy.get('#username').clear(); + cy.get('#username').type(username); + cy.contains('button', 'Sign In').click(); + }); + } + switchToLDAPAuth(): void { cy.get('#select-tab-ldap-login').click(); } diff --git a/yarn.lock b/yarn.lock index 8429339d0a..fd65852982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2465,6 +2465,36 @@ dependencies: easy-stack "1.0.1" +"@node-saml/node-saml@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@node-saml/node-saml/-/node-saml-5.1.0.tgz#43d61d4ea882f2960a44c7be5ae0030dafea2382" + integrity sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw== + dependencies: + "@types/debug" "^4.1.12" + "@types/qs" "^6.9.18" + "@types/xml-encryption" "^1.2.4" + "@types/xml2js" "^0.4.14" + "@xmldom/is-dom-node" "^1.0.1" + "@xmldom/xmldom" "^0.8.10" + debug "^4.4.0" + xml-crypto "^6.1.2" + xml-encryption "^3.1.0" + xml2js "^0.6.2" + xmlbuilder "^15.1.1" + xpath "^0.0.34" + +"@node-saml/passport-saml@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@node-saml/passport-saml/-/passport-saml-5.1.0.tgz#34d6af0284d489814122b39b4de929f14d04ab98" + integrity sha512-pBm+iFjv9eihcgeJuSUs4c0AuX1QEFdHwP8w1iaWCfDzXdeWZxUBU5HT2bY2S4dvNutcy+A9hYsH7ZLBGtgwDg== + dependencies: + "@node-saml/node-saml" "^5.1.0" + "@types/express" "^4.17.23" + "@types/passport" "^1.0.17" + "@types/passport-strategy" "^0.2.38" + passport "^0.7.0" + passport-strategy "^1.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3748,7 +3778,7 @@ resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== -"@types/debug@^4.0.0", "@types/debug@^4.1.8": +"@types/debug@^4.0.0", "@types/debug@^4.1.12", "@types/debug@^4.1.8": version "4.1.13" resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz#22d1cc9d542d3593caea764f974306ab36286ee7" integrity sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw== @@ -3853,7 +3883,7 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@^4.17.13": +"@types/express@^4.17.13", "@types/express@^4.17.23": version "4.17.25" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== @@ -4100,7 +4130,7 @@ "@types/oauth" "*" "@types/passport" "*" -"@types/passport-strategy@*": +"@types/passport-strategy@*", "@types/passport-strategy@^0.2.38": version "0.2.38" resolved "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== @@ -4108,7 +4138,7 @@ "@types/express" "*" "@types/passport" "*" -"@types/passport@*": +"@types/passport@*", "@types/passport@^1.0.17": version "1.0.17" resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== @@ -4147,6 +4177,11 @@ resolved "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz#963ab61779843fe910639a50661b48f162bc7f79" integrity sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow== +"@types/qs@^6.9.18": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + "@types/range-parser@*": version "1.2.7" resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" @@ -4276,7 +4311,14 @@ dependencies: "@types/node" "*" -"@types/xml2js@^0.4.9": +"@types/xml-encryption@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/xml-encryption/-/xml-encryption-1.2.4.tgz#0eceea58c82a89f62c0a2dc383a6461dfc2fe1ba" + integrity sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q== + dependencies: + "@types/node" "*" + +"@types/xml2js@^0.4.14", "@types/xml2js@^0.4.9": version "0.4.14" resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a" integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ== @@ -4992,6 +5034,16 @@ "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" +"@xmldom/is-dom-node@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz#83b9f3e1260fb008061c6fa787b93a00f9be0629" + integrity sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q== + +"@xmldom/xmldom@^0.8.10", "@xmldom/xmldom@^0.8.5": + version "0.8.13" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz#00d1dd940b218dff2e49309d410d8bb212159225" + integrity sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw== + "@xmldom/xmldom@^0.9.7": version "0.9.10" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz#a0ad5a26fe8aa996310870726e1704977f769dee" @@ -9326,7 +9378,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@3.3.3, fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -19519,6 +19571,24 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== +xml-crypto@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-6.1.2.tgz#ed93e87d9538f92ad1ad2db442e9ec586723d07d" + integrity sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w== + dependencies: + "@xmldom/is-dom-node" "^1.0.1" + "@xmldom/xmldom" "^0.8.10" + xpath "^0.0.33" + +xml-encryption@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-3.1.0.tgz#f3e91c4508aafd0c21892151ded91013dcd51ca2" + integrity sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q== + dependencies: + "@xmldom/xmldom" "^0.8.5" + escape-html "^1.0.3" + xpath "0.0.32" + xml-formatter@^3.6.2: version "3.7.0" resolved "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.7.0.tgz#ac6f8c02e2388759652e9d0c8a4b5b3dfc0694e5" @@ -19554,7 +19624,7 @@ xml2js@^0.5.0: sax ">=0.6.0" xmlbuilder "~11.0.0" -xml2js@^0.6.0: +xml2js@^0.6.0, xml2js@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== @@ -19562,6 +19632,11 @@ xml2js@^0.6.0: sax ">=0.6.0" xmlbuilder "~11.0.0" +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlbuilder@~11.0.0: version "11.0.1" resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" @@ -19577,6 +19652,21 @@ xmlhttprequest@^1.8.0: resolved "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA== +xpath@0.0.32: + version "0.0.32" + resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" + integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== + +xpath@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07" + integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA== + +xpath@^0.0.34: + version "0.0.34" + resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.34.tgz#a769255e8816e0938e1e0005f2baa7279be8be12" + integrity sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA== + xss@^1.0.8: version "1.0.15" resolved "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a"