diff --git a/apps/backend/.env-example b/apps/backend/.env-example index 8bf9db4713..1a3d724fa2 100644 --- a/apps/backend/.env-example +++ b/apps/backend/.env-example @@ -38,6 +38,10 @@ DATABASE_SSL_CA= +## External interfaces +SPLUNK_HOST_URL= +TENABLE_HOST_URL= + # Authentication EXTERNAL_URL= diff --git a/apps/backend/config/app_config.ts b/apps/backend/config/app_config.ts index 44635051f5..b5066078a8 100644 --- a/apps/backend/config/app_config.ts +++ b/apps/backend/config/app_config.ts @@ -35,6 +35,24 @@ export default class AppConfig { return process.env[key] || this.envConfig[key]; } + getSplunkHostUrl(): string { + const splunk_host_url = this.get('SPLUNK_HOST_URL'); + if (splunk_host_url !== undefined) { + return splunk_host_url; + } else { + return ''; + } + } + + getTenableHostUrl(): string { + const tenable_host_url = this.get('TENABLE_HOST_URL'); + if (tenable_host_url !== undefined) { + return tenable_host_url; + } else { + return ''; + } + } + getDatabaseName(): string { const databaseName = this.get('DATABASE_NAME'); const nodeEnvironment = this.get('NODE_ENV'); diff --git a/apps/backend/src/config/config.service.ts b/apps/backend/src/config/config.service.ts index 2275a9715f..3bef773e05 100644 --- a/apps/backend/src/config/config.service.ts +++ b/apps/backend/src/config/config.service.ts @@ -57,10 +57,20 @@ export class ConfigService { oidcName: this.get('OIDC_NAME') || '', ldap: this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true' || false, registrationEnabled: this.isRegistrationAllowed(), - localLoginEnabled: this.isLocalLoginAllowed() + localLoginEnabled: this.isLocalLoginAllowed(), + tenableHostUrl: this.getTenableHostUrl(), + splunkHostUrl: this.getSplunkHostUrl() }); } + getSplunkHostUrl(): string { + return this.appConfig.getSplunkHostUrl(); + } + + getTenableHostUrl(): string { + return this.appConfig.getTenableHostUrl(); + } + getDbConfig(): SequelizeOptions { return this.appConfig.getDbConfig(); } diff --git a/apps/backend/src/config/dto/startup-settings.dto.ts b/apps/backend/src/config/dto/startup-settings.dto.ts index c6069b5597..6329e3c9ba 100644 --- a/apps/backend/src/config/dto/startup-settings.dto.ts +++ b/apps/backend/src/config/dto/startup-settings.dto.ts @@ -11,6 +11,8 @@ export class StartupSettingsDto implements IStartupSettings { readonly ldap: boolean; readonly registrationEnabled: boolean; readonly localLoginEnabled: boolean; + readonly tenableHostUrl: string; + readonly splunkHostUrl: string; constructor(settings: IStartupSettings) { this.apiKeysEnabled = settings.apiKeysEnabled; @@ -23,5 +25,7 @@ export class StartupSettingsDto implements IStartupSettings { this.ldap = settings.ldap; this.registrationEnabled = settings.registrationEnabled; this.localLoginEnabled = settings.localLoginEnabled; + this.tenableHostUrl = settings.tenableHostUrl; + this.splunkHostUrl = settings.splunkHostUrl; } } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 8b92c75b95..9d9ecf76d8 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -40,8 +40,10 @@ async function bootstrap() { 'connect-src': [ "'self'", 'https://api.github.com', - 'https://sts.amazonaws.com' - ] + 'https://sts.amazonaws.com', + configService.getTenableHostUrl(), + configService.getSplunkHostUrl() + ].filter((source) => source) } }) ); diff --git a/apps/frontend/src/components/global/upload_tabs/tenable/AuthStep.vue b/apps/frontend/src/components/global/upload_tabs/tenable/AuthStep.vue index 5622990c43..48991f7ef9 100644 --- a/apps/frontend/src/components/global/upload_tabs/tenable/AuthStep.vue +++ b/apps/frontend/src/components/global/upload_tabs/tenable/AuthStep.vue @@ -2,6 +2,7 @@
{ + if (!this.accesskey) { + SnackbarModule.failure('The Access Token (key) is required'); + this.$refs.access_Key.focus(); + return; + } else if (!this.secretkey) { + SnackbarModule.failure('The Secret Token (key) is required'); + this.$refs.secret_Key.focus(); + return; + } else if (!this.hostname) { + SnackbarModule.failure('The Tenable.Sc URL is required'); + this.$refs.hostname_value.focus(); + return; + } + + // If the protocol (https) is missing add it if (!/^https?:\/\//.test(this.hostname)) { this.hostname = `https://${this.hostname}`; } + // If the SSL/TLS port is missing add default 443 + if (!this.hostname.split(':')[2]) { + this.hostname = `${this.hostname}:443`; + } + const config: AuthInfo = { accesskey: this.accesskey, secretkey: this.secretkey, diff --git a/apps/frontend/src/components/global/upload_tabs/tenable/TenableReader.vue b/apps/frontend/src/components/global/upload_tabs/tenable/TenableReader.vue index 8bb165bf2c..b516071805 100644 --- a/apps/frontend/src/components/global/upload_tabs/tenable/TenableReader.vue +++ b/apps/frontend/src/components/global/upload_tabs/tenable/TenableReader.vue @@ -41,7 +41,7 @@
- For connection instructions and further information, check here: + For connection instructions and further information, consult: reject( new Error( - 'Login timed out. Please check your CORS configuration or validate you have inputted the correct domain' + 'Login timed out. Please ensure the provided credentials and domain/URL are valid and try again.' ) ), 5000 @@ -64,21 +65,7 @@ export class TenableUtil { resolve(response.request.finished); }) .catch((error) => { - try { - if (error.code == 'ENOTFOUND') { - reject( - `Host: ${this.hostConfig.host_url} not found, check the Host Name (URL) or the network` - ); - } else if (error.response.data.error_code == 74) { - reject('Incorrect Access or Secret key'); - } else { - reject(error.response.data.error_msg); - } - } catch (e) { - reject( - `Possible network connection blocked by CORS policy. Received error: ${error}` - ); - } + reject(this.getRejectConnectionMessage(error)); }); } catch (e) { reject(`Unknown error: ${e}`); @@ -86,6 +73,63 @@ export class TenableUtil { }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getRejectConnectionMessage(error: any): string { + let rejectMsg = ''; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + + if (error.response.data.error_code == 74) { + rejectMsg = 'Incorrect Access or Secret key'; + } else { + rejectMsg = `${error.name} : ${error.response.data.error_msg}`; + } + } else if (error.request) { + // The request was made but no response was received. + // `error.request` is an instance of XMLHttpRequest in the + // browser and an instance of http.ClientRequest in node.js + + if (error.code == 'ERR_NETWORK') { + // Check if the tenable url was provided - Content Security Policy (CSP) + const corsReject = `Access blocked by CORS or connection refused by the host: ${error.config.baseURL}. See Help for additional instructions.`; + const tenableUrl = ServerModule.tenableHostUrl; + if (tenableUrl) { + // If the URL is listed in the allows domains + // (.env variable TENABLE_HOST_URL) check if they match + if (!error.config.baseURL.includes(tenableUrl)) { + rejectMsg = `Hostname: ${error.config.baseURL} violates the Content Security Policy (CSP). The host allowed by the CSP is: ${tenableUrl}`; + } else { + // CSP url did match, check for port match - reject appropriately + const portNumber = parseInt(this.hostConfig.host_url.split(':')[2]); + if (portNumber != 443) { + rejectMsg = `Invalid SSL/TSL port number used: ${portNumber} must be 443.`; + } else { + rejectMsg = corsReject; + } + } + } else if (ServerModule.serverMode) { + // The URL is not listed in the allows domains (CSP) and Heimdall instance is a server + rejectMsg = + 'The Content Security Policy directive environment variable "TENABLE_HOST_URL" not configured. See Help for additional instructions.'; + } else { + rejectMsg = corsReject; + } + } else if (error.code == 'ENOTFOUND') { + rejectMsg = `Host: ${error.config.baseURL} not found, check the Hostname (URL) or the network.`; + } else if (error.code == 'ERR_CONNECTION_REFUSED') { + rejectMsg = `Received network connection refused by the host: ${error.config.baseURL}`; + } else { + rejectMsg = `${error.name} : ${error.message}`; + } + } else { + // Something happened in setting up the request that triggered an Error + rejectMsg = `${error.name} : ${error.message}`; + } + return rejectMsg; + } + /** * Gets the list of Scan Results. * Returned values are based on the fields requested: @@ -98,7 +142,7 @@ export class TenableUtil { () => reject( new Error( - 'Login timed out. Please check your CORS configuration or validate you have inputted the correct domain' + 'Login timed out. Please ensure the provided credentials and domain/URL are valid and try again.' ) ), 5000 @@ -113,11 +157,7 @@ export class TenableUtil { resolve(response.data.response.usable); }) .catch((error) => { - if (error.response.data.error_code == 74) { - reject('Incorrect Access or Secret key'); - } else { - reject(error.response.data.error_msg); - } + reject(`${error.name} : ${error.message}`); }); } catch (e) { reject(e); @@ -140,7 +180,7 @@ export class TenableUtil { () => reject( new Error( - 'Login timed out. Please check your CORS configuration or validate you have inputted the correct domain' + 'Login timed out. Please check your CORS configuration and validate that the hostname is correct.' ) ), 5000 diff --git a/libs/interfaces/config/startup-settings.interface.ts b/libs/interfaces/config/startup-settings.interface.ts index c304fadc0d..c9b7352a34 100644 --- a/libs/interfaces/config/startup-settings.interface.ts +++ b/libs/interfaces/config/startup-settings.interface.ts @@ -9,4 +9,6 @@ export interface IStartupSettings { readonly ldap: boolean; readonly registrationEnabled: boolean; readonly localLoginEnabled: boolean; + readonly tenableHostUrl: string; + readonly splunkHostUrl: string; }