Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38faecc
wip: local saml standup files
seanlongcc May 5, 2026
580d153
feat: initial scaffolding for saml auth
seanlongcc May 7, 2026
a04789a
wip: added saml to authn.controller and authn.module
owilliams23 May 11, 2026
6e5c308
lint: lint oidc strategy for my eyes
seanlongcc May 18, 2026
7467328
fix: initial saml callback
seanlongcc May 20, 2026
a805919
feat: proper saml user handling
seanlongcc May 21, 2026
0ef729f
fix: changed saml logo
seanlongcc May 27, 2026
309fc2c
fix: corrected strange mispellings causing frontend to not build
seanlongcc May 27, 2026
6438a31
chore: update login provider icons
seanlongcc May 27, 2026
bbe99f0
chore: added back removed github icons
seanlongcc May 27, 2026
84861a6
feat: saml buttom implementation
owilliams23 Jun 2, 2026
045e3dc
fix: pass var for document signature
seanlongcc Jun 2, 2026
10c0866
chore: remove comments
seanlongcc Jun 3, 2026
38ae038
fix: variable claim names
seanlongcc Jun 4, 2026
44a93fa
style: revert formatting changes
seanlongcc Jun 4, 2026
6e8a2d9
fix: change saml svg and use upstream object
seanlongcc Jun 8, 2026
6df0f67
feat: modular claim name
seanlongcc Jun 8, 2026
19c0617
chore: licensing for logos
seanlongcc Jun 10, 2026
194ca50
fix: removed helper function to improve style
owilliams23 Jun 17, 2026
e15f1f5
refactor: consolidate enabled auth strategies
seanlongcc Jun 22, 2026
5f1e808
refactor: centralize auth strategy constants
seanlongcc Jun 23, 2026
86f6666
fix: index.js provides actual runtime constants
seanlongcc Jun 26, 2026
bb4f1bc
fix: updated config support for saml
owilliams23 Jun 29, 2026
6d63f92
fix: correct saml defaults
seanlongcc Jul 2, 2026
37298c6
fix: reduce cognitive complexity for samlscoping
seanlongcc Jul 6, 2026
dd76990
test: added saml tests
seanlongcc Jul 6, 2026
ce069e3
Merge remote-tracking branch 'origin/master' into feat/saml-auth
seanlongcc Jul 6, 2026
9c740c2
test: use secrets for mocksaml keys
seanlongcc Jul 6, 2026
5d5a8c5
fix: fix saml sign in
seanlongcc Jul 6, 2026
e01291e
fix: rename function to better reflect action
seanlongcc Jul 6, 2026
390f364
fix: support nested and array SAML claim attributes
seanlongcc Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/e2e-ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 22 additions & 1 deletion apps/backend/src/authn/authn.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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: {
Expand All @@ -187,4 +208,4 @@ export class AuthnController {
});
req.res?.redirect('/');
}
}
}
4 changes: 3 additions & 1 deletion apps/backend/src/authn/authn.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Agent> {
const {HttpsProxyAgent} = await import('https-proxy-agent');
Expand All @@ -45,6 +46,7 @@ async function buildHttpsProxyAgent(proxyUrl: string): Promise<Agent> {
GoogleStrategy,
LDAPStrategy,
ApiKeyService,
SAMLStrategy,
{
provide: OidcStrategy,
useFactory: async (
Expand Down Expand Up @@ -77,4 +79,4 @@ async function buildHttpsProxyAgent(proxyUrl: string): Promise<Agent> {
],
controllers: [AuthnController]
})
export class AuthnModule {}
export class AuthnModule {}
3 changes: 2 additions & 1 deletion apps/backend/src/authn/authn.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {AuthStrategy} from '@heimdall/common/interfaces';
import {
ForbiddenException,
Injectable,
Expand Down Expand Up @@ -100,7 +101,7 @@ export class AuthnService {
email: string,
firstName: string,
lastName: string,
creationMethod: string
creationMethod: AuthStrategy
): Promise<User> {
let user: User;
try {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/authn/oidc.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,4 @@ export class OidcStrategy extends PassportStrategy(Strategy as any, 'oidc') {
)
);
}
}
}
21 changes: 21 additions & 0 deletions apps/backend/src/authn/resolve_claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { UnauthorizedException } from '@nestjs/common';
import _ from 'lodash';

Check warning on line 2 in apps/backend/src/authn/resolve_claim.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Import lodash methods from subpaths such as "lodash/map" to avoid importing more code than needed.

See more on https://sonarcloud.io/project/issues?id=mitre_heimdall2&issues=AZ89Od2RpkQyqPAdTV0f&open=AZ89Od2RpkQyqPAdTV0f&pullRequest=8201

export function getRequiredClaim(
claims: Record<string, unknown>,
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}".`);
}
134 changes: 134 additions & 0 deletions apps/backend/src/authn/saml.strategy.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure to follow along with all the other locations that LDAP does stuff, ex:

  • the apps/backend/src/users/dto/create-user.dto.ts
  • example envvars file
  • the helm chart would need to be updated
  • the environment variables wiki page
  • tests (https://github.com/ory/mocksaml)

https://github.com/search?q=repo%3Amitre%2Fheimdall2+ldap&type=code&p=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonblocking: consider making a helper function that returns all of the SAML envvars that we need to do stuff with SAML, ex. gets the envvar (which we will need to create) that names the attribute that we will use to do 'given_name' / first_name claim equivalent

it should also handle default values for stuff that was not provided by the user

ex. user provides SAML_IDENTIFIER_FORMAT but not SAML_IDP_ISSUER. is there a default value that we can provide there? otherwise this function should throw an error that explains what happened.

we will then call the function within the constructor in order to make it fill out the information and throw the error at the time of construction if all of our needs are not satisfied.

const getSAMLInformation = (configService: ConfigService): Record<string, string> => {
  return {
    SAML_IDENTIFIER_FORMAT: configService.get('SAML_IDENTIFIER_FORMAT') || defaultvalue,
    SAML_IDP_ISSUER: configService.get('SAML_IDP_ISSUER') || throw Error('value not provided'),
    SAML_FIRST_NAME_CLAIM: configService.get('SAML_FIRST_NAME_CLAIM') || 'attributes.firstName'
    ...
  };
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you wanted to be fancy instead of returning a record<string, string> you could define a type that looks like {SAML_IDENTIFIER_FORMAT: string, blah blah}.

Original file line number Diff line number Diff line change
@@ -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') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to remove 'as any' cause sometimes the strategy 'shape'/type matches the new passportstrategy type. other times, especially older / less maintained strategies need to use the 'as any' escape mechanism.

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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add support for all the other config options for SAML

especially make sure that any of the certs / proxies / etc options are defined

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<User> {
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',
);
}
}
55 changes: 35 additions & 20 deletions apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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',
Expand Down Expand Up @@ -94,10 +116,3 @@ export class ConfigService {
return this.appConfig.get(key);
}
}
export const supportedOauth: string[] = [
'github',
'gitlab',
'google',
'okta',
'oidc'
];
14 changes: 5 additions & 9 deletions apps/backend/src/config/dto/startup-settings.dto.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import {IStartupSettings} from '@heimdall/common/interfaces';
import type {AuthStrategy, IStartupSettings} from '@heimdall/common/interfaces';

export class StartupSettingsDto implements 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;

constructor(settings: IStartupSettings) {
this.apiKeysEnabled = settings.apiKeysEnabled;
this.banner = settings.banner;
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;
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading