From 65f5d12cdd74a3da04c928a87eab35b7bba44832 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 13 Dec 2023 17:34:58 +0100 Subject: [PATCH] fix(kibana-security-health-check): improve "Dashboard" tracker view --- kibana-security-health-check/src/dashboard.ts | 137 +++++++++++++++--- kibana-security-health-check/src/index.ts | 21 +++ 2 files changed, 141 insertions(+), 17 deletions(-) diff --git a/kibana-security-health-check/src/dashboard.ts b/kibana-security-health-check/src/dashboard.ts index f92e207..d8a3804 100644 --- a/kibana-security-health-check/src/dashboard.ts +++ b/kibana-security-health-check/src/dashboard.ts @@ -1,4 +1,9 @@ -import type { KibanaMetadata } from './index'; +import type { + Credentials, + KibanaMetadata, + SecurityResponseHeaders, + SecutilsGetContentSecurityPolicyResponse, +} from './index'; interface Meta { lastRevisionId: string; @@ -8,8 +13,16 @@ const META_REGEX = /\(https:\/\/meta.secutils.dev\/(.+)\)/gm; interface Params { targetContentTrackerId: string; - targetCspShareId: string; - credentials: { username: string; password: string }; + credentials: Credentials; + expected: { + contentSecurityPolicyId: string; + crossOriginOpenerPolicy: string; + permissionsPolicy: string; + referrerPolicy: string; + strictTransportSecurity: string; + xContentTypeOptions: string; + xFrameOptions: string; + }; } interface WebPageContentRevision { @@ -38,27 +51,84 @@ export async function run(previousContent: string | undefined, params: Params): } const lastRevision = revisions[revisions.length - 1]; - const kibanaMetadata = (JSON.parse(lastRevision.data) as { injectedMetadata: KibanaMetadata }).injectedMetadata; + if (previousContent && lastRevision.id === previousMeta?.lastRevisionId) { + return previousContent; + } + + const expectedCsp = await getExpectedContentSecurityPolicy( + params.credentials, + params.expected.contentSecurityPolicyId, + ); + + const { injectedMetadata, headers: responseHeaders } = JSON.parse(lastRevision.data) as { + headers: SecurityResponseHeaders; + injectedMetadata: KibanaMetadata; + }; const state = ` # Project information ||| | ------ | ----------- | -| **Environment** | ${kibanaMetadata.env.mode.name} | -| **Branch** | ${kibanaMetadata.env.packageInfo.branch} | -| **Project ID / Cluster Name** | ${kibanaMetadata.clusterInfo?.cluster_name ?? '?'} | -| **Build Flavour** | ${kibanaMetadata.env.packageInfo.buildFlavor} | -| **Build Date** | ${kibanaMetadata.env.packageInfo.buildDate} | -| **Build Number** | ${kibanaMetadata.env.packageInfo.buildNum} | -| **Build Commit** | [${kibanaMetadata.env.packageInfo.buildSha.slice( +| **Environment** | ${injectedMetadata.env.mode.name} | +| **Branch** | ${injectedMetadata.env.packageInfo.branch} | +| **Project ID / Cluster Name** | ${injectedMetadata.clusterInfo?.cluster_name ?? '?'} | +| **Build Flavour** | ${injectedMetadata.env.packageInfo.buildFlavor} | +| **Build Date** | ${injectedMetadata.env.packageInfo.buildDate} | +| **Build Number** | ${injectedMetadata.env.packageInfo.buildNum} | +| **Build Commit** | [${injectedMetadata.env.packageInfo.buildSha.slice( 6, - )}](https://github.com/elastic/kibana/commit/${kibanaMetadata.env.packageInfo.buildSha}) | -| **Version** | ${kibanaMetadata.env.packageInfo.version}| + )}](https://github.com/elastic/kibana/commit/${injectedMetadata.env.packageInfo.buildSha}) | +| **Version** | ${injectedMetadata.env.packageInfo.version}| # Security headers -## Content Security Policy -Status: ${ - lastRevision.id === previousMeta?.lastRevisionId || !previousMeta ? ':white_check_mark:' : ':red_circle:' - } [view policy](${location.origin}/ws/web_security__csp__policies?x-user-share-id=${params.targetCspShareId}) +## ${ + responseHeaders['content-security-policy'] === expectedCsp.policyText ? ':white_check_mark:' : ':red_circle:' + } Content Security Policy +\`\`\` +${responseHeaders['content-security-policy']} +\`\`\` +[**:mag_right: Inspect**](${location.origin}/ws/web_security__csp__policies?x-user-share-id=${expectedCsp.userShareId}) +## ${ + responseHeaders['cross-origin-opener-policy'] === params.expected.crossOriginOpenerPolicy + ? ':white_check_mark:' + : ':red_circle:' + } Cross Origin Opener Policy +\`\`\` +${responseHeaders['cross-origin-opener-policy']} +\`\`\` +## ${ + responseHeaders['permissions-policy'] === params.expected.permissionsPolicy ? ':white_check_mark:' : ':red_circle:' + } Permissions Policy +\`\`\` +${responseHeaders['permissions-policy']} +\`\`\` +## ${ + responseHeaders['referrer-policy'] === params.expected.referrerPolicy ? ':white_check_mark:' : ':red_circle:' + } Referrer Policy +\`\`\` +${responseHeaders['referrer-policy']} +\`\`\` +## ${ + responseHeaders['strict-transport-security'] === params.expected.strictTransportSecurity + ? ':white_check_mark:' + : ':red_circle:' + } Strict Transport Security Policy +\`\`\` +${responseHeaders['strict-transport-security']} +\`\`\` +## ${ + responseHeaders['x-content-type-options'] === params.expected.xContentTypeOptions + ? ':white_check_mark:' + : ':red_circle:' + } Content Type Options +\`\`\` +${responseHeaders['x-content-type-options']} +\`\`\` +## ${ + responseHeaders['x-frame-options'] === params.expected.xFrameOptions ? ':white_check_mark:' : ':red_circle:' + } Frame Options +\`\`\` +${responseHeaders['x-frame-options']} +\`\`\` `; return prependMeta(state, { @@ -77,3 +147,36 @@ function prependMeta(markdownState: string, meta: Meta): string { function extractMeta(markdownState: string): Meta { return JSON.parse(decodeURIComponent(Array.from(markdownState.matchAll(META_REGEX))[0][1])) as Meta; } + +async function getExpectedContentSecurityPolicy( + credentials: Credentials, + contentSecurityPolicyId: string, +): Promise<{ userShareId: string; policyText: string }> { + const authorizationHeader = `Basic ${btoa(`${credentials.username}:${credentials.password}`)}`; + // Retrieve policy to get user share ID. + const getResponse = (await ( + await fetch(`${location.origin}/api/utils/web_security/csp/${contentSecurityPolicyId}`, { + credentials: 'omit', + headers: { Authorization: authorizationHeader, Accept: 'application/json' }, + }) + ).json()) as SecutilsGetContentSecurityPolicyResponse; + + if (!getResponse.userShare) { + throw new Error(`Could not find user share for policy ${contentSecurityPolicyId}`); + } + + // Fetch serialized policy + const serializeResponse = await ( + await fetch('https://dev.secutils.dev/api/utils/web_security/csp/018c5b81-2908-7583-99e5-0c03fc9f827e/serialize', { + credentials: 'omit', + headers: { Authorization: authorizationHeader, Accept: 'text/plain, */*', 'Content-Type': 'application/json' }, + body: JSON.stringify({ source: 'enforcingHeader' }), + method: 'POST', + }) + ).text(); + + return { + userShareId: getResponse.userShare.id, + policyText: serializeResponse, + }; +} diff --git a/kibana-security-health-check/src/index.ts b/kibana-security-health-check/src/index.ts index 085a445..9ad60e4 100644 --- a/kibana-security-health-check/src/index.ts +++ b/kibana-security-health-check/src/index.ts @@ -1,6 +1,11 @@ export { run as dashboardRun } from './dashboard'; export { run as trackerRun } from './tracker'; +export interface Credentials { + username: string; + password: string; +} + export interface WebPageResource { url: string; data: string; @@ -40,3 +45,19 @@ export interface KibanaMetadata { }; }; } + +export interface SecutilsGetContentSecurityPolicyResponse { + policy: SecutilsContentSecurityPolicy; + userShare?: SecutilsUserShare; +} + +export interface SecutilsContentSecurityPolicy { + id: string; + name: string; + createdAt: number; +} + +export interface SecutilsUserShare { + id: string; + createdAt: number; +}