Skip to content

Commit

Permalink
updateTenableContentSecurityPolicy (#6135)
Browse files Browse the repository at this point in the history
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Amndeep Singh Mann <[email protected]>
  • Loading branch information
3 people authored and aaronlippold committed Nov 20, 2024
1 parent 950f956 commit 93942bf
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 27 deletions.
4 changes: 4 additions & 0 deletions apps/backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ DATABASE_SSL_CA=<Full path to SSL certificate authority OR the certificate autho
## Reverse proxy
NGINX_HOST=<Templated out as the 'server_name' for the NGINX configuration (no default, must be set if using the provided example NGINX configuration)>

## External interfaces
SPLUNK_HOST_URL=<The full Uniform Resource Locator (URL) without the port for the Splunk host (no default, must be set if connecting to Splunk)>
TENABLE_HOST_URL=<The full Uniform Resource Locator (URL) without the port for the Tenable.SC host (no default, must be set if connecting to Tenable)>

# Authentication

EXTERNAL_URL=<The external URL for your Heimdall deployment, for example https://heimdall.mitre.org>
Expand Down
18 changes: 18 additions & 0 deletions apps/backend/config/app_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
12 changes: 11 additions & 1 deletion apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/config/dto/startup-settings.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
6 changes: 4 additions & 2 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
<div>
<v-form>
<v-text-field
ref="access_Key"
v-model="accesskey"
label="Access Token (Key)"
for="accesskey_field"
:rules="[reqRule]"
data-cy="tenableaccesskey"
/>
<v-text-field
ref="secret_Key"
v-model="secretkey"
label="Secret Token (Key)"
for="secretkey_field"
Expand All @@ -18,6 +20,7 @@
data-cy="tenablesecretkey"
/>
<v-text-field
ref="hostname_value"
v-model="hostname"
label="Hostname"
for="hostname_field"
Expand Down Expand Up @@ -70,14 +73,40 @@ export default class AuthStep extends Vue {
secretkey = '';
hostname = '';
$refs!: {
access_Key: HTMLInputElement;
secret_Key: HTMLAnchorElement;
hostname_value: HTMLAnchorElement;
};
// Form required field rule
reqRule = requireFieldRule;
async login(): Promise<void> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</span>
<br />
<span>
For connection instructions and further information, check here:
For connection instructions and further information, consult:
</span>
<v-btn
target="_blank"
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/store/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface IServerState {
ldap: boolean;
localLoginEnabled: boolean;
userInfo: IUser;
tenableHostUrl: string;
splunkHostUrl: string;
}

interface LoginData {
Expand Down Expand Up @@ -63,6 +65,8 @@ class Server extends VuexModule implements IServerState {
enabledOAuth: string[] = [];
allUsers: ISlimUser[] = [];
oidcName = '';
tenableHostUrl: string = '';
splunkHostUrl: string = '';
/** Our currently granted JWT token */
token = '';
/** Provide a sane default for userInfo in order to avoid having to null check it all the time */
Expand Down Expand Up @@ -106,6 +110,8 @@ class Server extends VuexModule implements IServerState {
this.oidcName = settings.oidcName;
this.ldap = settings.ldap;
this.localLoginEnabled = settings.localLoginEnabled;
this.tenableHostUrl = settings.tenableHostUrl;
this.splunkHostUrl = settings.splunkHostUrl;
}

@Mutation
Expand Down
86 changes: 63 additions & 23 deletions apps/frontend/src/utilities/tenable_util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Zip from 'adm-zip';
import axios, {AxiosInstance} from 'axios';
import {ServerModule} from '@/store/server';
import {createWinstonLogger} from '../../../../libs/hdf-converters/src/utils/global';

/** represents the information of the current used */
Expand Down Expand Up @@ -49,7 +50,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
Expand All @@ -64,28 +65,71 @@ 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}`);
}
});
}

// 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:
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions libs/interfaces/config/startup-settings.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface IStartupSettings {
readonly ldap: boolean;
readonly registrationEnabled: boolean;
readonly localLoginEnabled: boolean;
readonly tenableHostUrl: string;
readonly splunkHostUrl: string;
}

0 comments on commit 93942bf

Please sign in to comment.