<script> import { GlLink, GlSprintf, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue'; import { __ } from '~/locale'; import DetailItem from './detail_item.vue'; export default { name: 'VulnerabilityDetails', components: { CodeBlock, GlLink, SeverityBadge, DetailItem, GlSprintf, GlIcon }, directives: { GlTooltip: GlTooltipDirective, }, props: { vulnerability: { type: Object, required: true, }, }, computed: { location() { return this.vulnerability.location || {}; }, stacktraceSnippet() { return this.vulnerability.stacktraceSnippet || ''; }, scanner() { return this.vulnerability.scanner || {}; }, fileText() { return (this.location.file || '') + (this.lineNumber ? `:${this.lineNumber}` : ''); }, fileUrl() { return (this.location.blobPath || '') + (this.lineNumber ? `#L${this.lineNumber}` : ''); }, lineNumber() { const { startLine: start, endLine: end } = this.location; return end > start ? `${start}-${end}` : start; }, scannerUrl() { return this.scanner.url || ''; }, scannerDetails() { if (this.scannerUrl) { return { component: 'GlLink', properties: { href: this.scannerUrl, target: '_blank', }, }; } return { component: 'span', properties: {}, }; }, assertion() { return this.vulnerability.evidenceSource?.name; }, recordedMessage() { return this.vulnerability?.supportingMessages?.find( msg => msg.name === SUPPORTING_MESSAGE_TYPES.RECORDED, )?.response; }, constructedRequest() { return this.constructRequest(this.vulnerability.request); }, constructedResponse() { return this.constructResponse(this.vulnerability.response); }, constructedRecordedResponse() { return this.constructResponse(this.recordedMessage); }, requestData() { if (!this.vulnerability.request) { return []; } return [ { label: __('%{labelStart}Sent request:%{labelEnd} %{headers}'), content: this.constructedRequest, isCode: true, }, ].filter(x => x.content); }, responseData() { if (!this.vulnerability.response) { return []; } return [ { label: __('%{labelStart}Actual response:%{labelEnd} %{headers}'), content: this.constructedResponse, isCode: true, }, ].filter(x => x.content); }, recordedResponseData() { if (!this.recordedMessage) { return []; } return [ { label: __('%{labelStart}Unmodified response:%{labelEnd} %{headers}'), content: this.constructedRecordedResponse, isCode: true, }, ].filter(x => x.content); }, shouldShowLocation() { return ( this.location.crashAddress || this.location.crashType || this.location.stacktraceSnippet || this.location.file || this.location.image || this.location.operatingSystem ); }, hasRequest() { return Boolean(this.requestData.length); }, hasResponse() { return Boolean(this.responseData.length); }, hasRecordedResponse() { return Boolean(this.recordedResponseData.length); }, hasResponses() { return Boolean(this.hasResponse || this.hasRecordedResponse); }, }, methods: { getHeadersAsCodeBlockLines(headers) { return Array.isArray(headers) ? headers.map(({ name, value }) => `${name}: ${value}`).join('\n') : ''; }, constructResponse(response) { const { body, status_code: statusCode, reason_phrase: reasonPhrase, headers = [] } = response; const headerLines = this.getHeadersAsCodeBlockLines(headers); return statusCode && reasonPhrase && headerLines ? [`${statusCode} ${reasonPhrase}\n`, headerLines, '\n\n', body].join('') : ''; }, constructRequest(request) { const { body, method, url, headers = [] } = request; const headerLines = this.getHeadersAsCodeBlockLines(headers); return method && url && headerLines ? [`${method} ${url}\n`, headerLines, '\n\n', body].join('') : ''; }, }, }; </script> <template> <div class="md" data-qa-selector="vulnerability_details"> <h1 class="mt-3 mb-2 border-bottom-0" data-testid="title" data-qa-selector="vulnerability_title" > {{ vulnerability.title }} </h1> <h3 class="mt-0">{{ __('Description') }}</h3> <p data-testid="description" data-qa-selector="vulnerability_description"> {{ vulnerability.description }} </p> <ul> <detail-item :sprintf-message="__('%{labelStart}Severity:%{labelEnd} %{severity}')"> <severity-badge :severity="vulnerability.severity" class="gl-display-inline ml-1" /> </detail-item> <detail-item :sprintf-message="__('%{labelStart}Scan Type:%{labelEnd} %{reportType}')" >{{ vulnerability.reportType }} </detail-item> <detail-item v-if="scanner.name" :sprintf-message="__('%{labelStart}Scanner:%{labelEnd} %{scanner}')" > <component :is="scannerDetails.component" v-bind="scannerDetails.properties" data-testid="scannerSafeLink" > <gl-sprintf v-if="scanner.version" :message="s__('Vulnerability|%{scannerName} (version %{scannerVersion})')" > <template #scannerName>{{ scanner.name }}</template> <template #scannerVersion>{{ scanner.version }}</template> </gl-sprintf> <template v-else>{{ scanner.name }}</template> </component> </detail-item> <detail-item v-if="location.class" :sprintf-message="__('%{labelStart}Class:%{labelEnd} %{class}')" >{{ location.class }} </detail-item> <detail-item v-if="location.method" :sprintf-message="__('%{labelStart}Method:%{labelEnd} %{method}')" > <code>{{ location.method }}</code> </detail-item> <detail-item v-if="vulnerability.evidence" :sprintf-message="__('%{labelStart}Evidence:%{labelEnd} %{evidence}')" >{{ vulnerability.evidence }} </detail-item> </ul> <template v-if="shouldShowLocation"> <h3>{{ __('Location') }}</h3> <ul> <detail-item v-if="location.image" :sprintf-message="__('%{labelStart}Image:%{labelEnd} %{image}')" >{{ location.image }} </detail-item> <detail-item v-if="location.operatingSystem" :sprintf-message="__('%{labelStart}Namespace:%{labelEnd} %{namespace}')" >{{ location.operatingSystem }} </detail-item> <detail-item v-if="location.file" :sprintf-message="__('%{labelStart}File:%{labelEnd} %{file}')" > <gl-link :href="fileUrl" target="_blank">{{ fileText }}</gl-link> </detail-item> <detail-item v-if="location.crashAddress" :sprintf-message="__('%{labelStart}Crash Address:%{labelEnd} %{crash_address}')" >{{ location.crashAddress }} </detail-item> <detail-item v-if="location.stacktraceSnippet" :sprintf-message="__('%{labelStart}Crash State:%{labelEnd} %{stacktrace_snippet}')" > <code-block :code="location.stacktraceSnippet" max-height="225px" /> </detail-item> </ul> </template> <template v-if="vulnerability.links && vulnerability.links.length"> <h3>{{ __('Links') }}</h3> <ul> <li v-for="(link, index) in vulnerability.links" :key="`${index}:${link.url}`" class="gl-ml-0! gl-list-style-position-inside" > <gl-link :href="link.url" data-testid="link" target="_blank" :aria-label="__('Third Party Advisory Link')" :title="link.url" > {{ link.url }} </gl-link> </li> </ul> </template> <template v-if="vulnerability.identifiers && vulnerability.identifiers.length"> <h3>{{ __('Identifiers') }}</h3> <ul> <li v-for="(identifier, index) in vulnerability.identifiers" :key="`${index}:${identifier.url}`" class="gl-ml-0! gl-list-style-position-inside" > <gl-link :href="identifier.url" data-testid="identifier" target="_blank"> {{ identifier.name }} </gl-link> </li> </ul> </template> <section v-if="hasRequest" data-testid="request"> <h3>{{ s__('Vulnerability|Request/Response') }}</h3> <ul> <detail-item v-for="({ label, isCode, content }, index) in requestData" :key="`${index}:${label}`" :sprintf-message="label" > <code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" /> <template v-else> {{ content }} </template> </detail-item> </ul> </section> <div v-if="hasResponses" class="row"> <section v-if="hasRecordedResponse" :class="hasResponse ? 'col-6' : 'col'" data-testid="recorded-response" > <ul> <detail-item v-for="({ label, isCode, content }, index) in recordedResponseData" :key="`${index}:${label}`" :sprintf-message="label" > <gl-icon v-gl-tooltip name="information-o" class="gl-hover-cursor-pointer gl-mr-3" :title=" s__( 'Vulnerability|The unmodified response is the original response that had no mutations done to the request', ) " /> <code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" /> <template v-else> {{ content }} </template> </detail-item> </ul> </section> <section v-if="hasResponse" :class="hasRecordedResponse ? 'col-6' : 'col'" data-testid="response" > <ul> <detail-item v-for="({ label, isCode, content }, index) in responseData" :key="`${index}:${label}`" :sprintf-message="label" > <gl-icon v-gl-tooltip name="information-o" class="gl-hover-cursor-pointer gl-mr-3" :title=" s__( 'Vulnerability|Actual received response is the one received when this fault was detected', ) " /> <code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" /> <template v-else> {{ content }} </template> </detail-item> </ul> </section> </div> <template v-if="assertion"> <h3>{{ s__('Vulnerability|Additional Info') }}</h3> <ul> <detail-item :sprintf-message="__('%{labelStart}Assert:%{labelEnd} %{assertion}')"> {{ assertion }} </detail-item> </ul> </template> </div> </template>