vulnerability_details_spec.js 18.1 KB
Newer Older
1
import { GlLink } from '@gitlab/ui';
2
import { getAllByRole, getByTestId } from '@testing-library/dom';
3
import { mount, shallowMount } from '@vue/test-utils';
4
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
Jannik Lehmann's avatar
Jannik Lehmann committed
5
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
6
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
7
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
8 9 10 11 12 13 14

describe('Vulnerability Details', () => {
  let wrapper;

  const vulnerability = {
    severity: 'bad severity',
    confidence: 'high confidence',
15
    reportType: 'Some report type',
16
    description: 'vulnerability description',
17
    descriptionHtml: 'vulnerability description <code>sample</code>',
18
    identifiers: [],
19 20
  };

21
  const createWrapper = (vulnerabilityOverrides, { mountFn = mount } = {}) => {
22
    const propsData = {
23
      vulnerability: { ...vulnerability, ...vulnerabilityOverrides },
24
    };
25
    wrapper = mountFn(VulnerabilityDetails, {
26 27
      propsData,
    });
28
  };
29
  const createShallowWrapper = (...args) => createWrapper(...args, { mountFn: shallowMount });
30

31 32 33
  const getById = (id) => wrapper.find(`[data-testid="${id}"]`);
  const getAllById = (id) => wrapper.findAll(`[data-testid="${id}"]`);
  const getText = (id) => getById(id).text();
34 35 36 37 38 39 40

  afterEach(() => {
    wrapper.destroy();
  });

  it('shows the properties that should always be shown', () => {
    createWrapper();
41
    expect(getById('description').html()).toContain(vulnerability.descriptionHtml);
42
    expect(wrapper.findComponent(SeverityBadge).props('severity')).toBe(vulnerability.severity);
43
    expect(getText('reportType')).toBe(`Tool: ${vulnerability.reportType}`);
44

45
    expect(getById('title').exists()).toBe(false);
46 47 48 49 50 51 52
    expect(getById('image').exists()).toBe(false);
    expect(getById('os').exists()).toBe(false);
    expect(getById('file').exists()).toBe(false);
    expect(getById('class').exists()).toBe(false);
    expect(getById('method').exists()).toBe(false);
    expect(getById('evidence').exists()).toBe(false);
    expect(getById('scanner').exists()).toBe(false);
53 54 55 56
    expect(getAllById('link')).toHaveLength(0);
    expect(getAllById('identifier')).toHaveLength(0);
  });

57 58 59 60 61 62 63 64
  it('renders description when descriptionHtml is not present', () => {
    createWrapper({
      descriptionHtml: null,
    });
    expect(getById('description').html()).not.toContain(vulnerability.descriptionHtml);
    expect(getText('description')).toBe(vulnerability.description);
  });

65 66 67 68 69 70 71 72 73 74 75 76 77 78
  it.each`
    reportType                  | expectedOutput
    ${'SAST'}                   | ${'SAST'}
    ${'DAST'}                   | ${'DAST'}
    ${'DEPENDENCY_SCANNING'}    | ${'Dependency Scanning'}
    ${'CONTAINER_SCANNING'}     | ${'Container Scanning'}
    ${'SECRET_DETECTION'}       | ${'Secret Detection'}
    ${'COVERAGE_FUZZING'}       | ${'Coverage Fuzzing'}
    ${'API_FUZZING'}            | ${'API Fuzzing'}
    ${'CLUSTER_IMAGE_SCANNING'} | ${'Cluster Image Scanning'}
  `(
    'displays "$expectedOutput" when report type is "$reportType"',
    ({ reportType, expectedOutput }) => {
      createWrapper({ reportType });
79
      expect(getText('reportType')).toBe(`Tool: ${expectedOutput}`);
80 81 82 83 84 85 86 87
    },
  );

  it('shows the title if it exists', () => {
    createWrapper({ title: 'some title' });
    expect(getText('title')).toBe('some title');
  });

88 89 90 91 92 93
  it('shows the location image if it exists', () => {
    createWrapper({ location: { image: 'some image' } });
    expect(getText('image')).toBe(`Image: some image`);
  });

  it('shows the operating system if it exists', () => {
94
    createWrapper({ location: { operatingSystem: 'linux' } });
95 96 97
    expect(getText('namespace')).toBe(`Namespace: linux`);
  });

98
  it('shows the vulnerability class if it exists', () => {
99 100 101 102
    createWrapper({ location: { file: 'file', class: 'class name' } });
    expect(getText('class')).toBe(`Class: class name`);
  });

103
  it('shows the vulnerability method if it exists', () => {
104 105 106 107
    createWrapper({ location: { file: 'file', method: 'method name' } });
    expect(getText('method')).toBe(`Method: method name`);
  });

108 109 110 111 112
  it('shows the evidence if it exists', () => {
    createWrapper({ evidence: 'some evidence' });
    expect(getText('evidence')).toBe(`Evidence: some evidence`);
  });

113 114 115 116 117 118 119 120 121 122 123 124
  it('shows the links if they exist', () => {
    createWrapper({ links: [{ url: '0' }, { url: '1' }, { url: '2' }] });
    const links = getAllById('link');
    expect(links).toHaveLength(3);

    links.wrappers.forEach((link, index) => {
      expect(link.attributes('target')).toBe('_blank');
      expect(link.attributes('href')).toBe(index.toString());
      expect(link.text()).toBe(index.toString());
    });
  });

125
  it('shows the vulnerability identifiers if they exist', () => {
126 127 128 129 130 131 132 133 134
    const identifiersData = [
      { name: '00', url: 'http://example.com/00' },
      { name: '11', url: 'http://example.com/11' },
      { name: '22', url: 'http://example.com/22' },
      { name: '33' },
      { name: '44' },
      { name: '55' },
    ];

135
    createWrapper({
136
      identifiers: identifiersData,
137 138 139 140
    });

    const identifiers = getAllById('identifier');

141 142 143
    expect(identifiers).toHaveLength(identifiersData.length);

    const checkIdentifier = ({ name, url }, index) => {
144
      const identifier = identifiers.at(index);
145 146 147 148 149 150 151 152 153 154 155 156

      expect(identifier.text()).toBe(name);

      if (url) {
        expect(identifier.is(GlLink)).toBe(true);
        expect(identifier.attributes()).toMatchObject({
          target: '_blank',
          href: url,
        });
      } else {
        expect(identifier.is(GlLink)).toBe(false);
      }
157 158
    };

159
    identifiersData.forEach(checkIdentifier);
160 161
  });

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
  it('shows the vulnerability assets if they exist', () => {
    const assetsData = [
      { name: 'Postman Collection', url: 'http://example.com/postman' },
      { name: 'HTTP Messages', url: 'http://example.com/http-messages' },
      { name: 'Foo' },
      { name: 'Bar' },
    ];

    createWrapper({
      assets: assetsData,
    });

    const assets = getAllById('asset');

    expect(assets).toHaveLength(assetsData.length);

    const checkIdentifier = ({ name, url }, index) => {
      const asset = assets.at(index);

      expect(asset.text()).toBe(name);

      if (url) {
        expect(asset.is(GlLink)).toBe(true);
        expect(asset.attributes()).toMatchObject({
          target: '_blank',
          href: url,
        });
      } else {
        expect(asset.is(GlLink)).toBe(false);
      }
    };

    assetsData.forEach(checkIdentifier);
  });

197 198
  it('renders the vulnerabilityTraining component', () => {
    const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }];
199
    createShallowWrapper({ identifiers });
200 201 202 203 204
    expect(wrapper.findComponent(VulnerabilityTraining).props()).toMatchObject({
      identifiers,
    });
  });

205
  describe('file link', () => {
206
    const file = () => getById('file').findComponent(GlLink);
207 208

    it('shows only the file name if there is no start line', () => {
209
      createWrapper({ location: { file: 'test.txt', blobPath: 'blob_path.txt' } });
210 211 212 213 214 215
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob_path.txt');
      expect(file().text()).toBe('test.txt');
    });

    it('shows the correct line number when there is a start line', () => {
216
      createWrapper({ location: { file: 'test.txt', startLine: 24, blobPath: 'blob.txt' } });
217 218 219 220 221 222 223
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24');
      expect(file().text()).toBe('test.txt:24');
    });

    it('shows the correct line numbers when there is a start and end line', () => {
      createWrapper({
224
        location: { file: 'test.txt', startLine: 24, endLine: 27, blobPath: 'blob.txt' },
225 226 227 228 229 230 231 232
      });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24-27');
      expect(file().text()).toBe('test.txt:24-27');
    });

    it('shows only the start line when the end line is the same', () => {
      createWrapper({
233
        location: { file: 'test.txt', startLine: 24, endLine: 24, blobPath: 'blob.txt' },
234 235 236 237 238 239
      });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24');
      expect(file().text()).toBe('test.txt:24');
    });
  });
240 241 242 243 244 245

  describe('scanner', () => {
    const link = () => getById('scannerSafeLink');
    const scannerText = () => getById('scanner').text();

    it('shows the scanner name only but no link', () => {
246 247
      createWrapper({ scanner: { name: 'some scanner' } });
      expect(scannerText()).toBe('Scanner: some scanner');
248
      expect(link().element instanceof HTMLSpanElement).toBe(true);
249 250 251 252
    });

    it('shows the scanner name and version but no link', () => {
      createWrapper({ scanner: { name: 'some scanner', version: '1.2.3' } });
253
      expect(scannerText()).toBe('Scanner: some scanner (version 1.2.3)');
254
      expect(link().element instanceof HTMLSpanElement).toBe(true);
255 256 257
    });

    it('shows the scanner name only with a link', () => {
258
      createWrapper({ scanner: { name: 'some tool', url: '//link' } });
259
      expect(scannerText()).toBe('Scanner: some tool');
260
      expect(link().attributes('href')).toBe('//link');
261 262 263
    });

    it('shows the scanner name and version with a link', () => {
264
      createWrapper({ scanner: { name: 'some tool', version: '1.2.3', url: '//link' } });
265
      expect(scannerText()).toBe('Scanner: some tool (version 1.2.3)');
266
      expect(link().attributes('href')).toBe('//link');
267 268
    });
  });
269 270

  describe('http data', () => {
271 272 273 274
    const TEST_HEADERS = [
      { name: 'Name1', value: 'Value1' },
      { name: 'Name2', value: 'Value2' },
    ];
Fernando's avatar
Fernando committed
275 276 277 278 279 280
    const EXPECT_REQUEST = {
      label: 'Sent request:',
      content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

281 282 283 284 285 286 287
    const EXPECT_REQUEST_WITHOUT_BODY = {
      label: 'Sent request:',
      content:
        'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

288 289 290 291 292 293
    const EXPECT_REQUEST_WITH_EMPTY_STRING = {
      label: 'Sent request:',
      content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2',
      isCode: true,
    };

Fernando's avatar
Fernando committed
294 295
    const EXPECT_RESPONSE = {
      label: 'Actual response:',
Fernando's avatar
Fernando committed
296
      content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
Fernando's avatar
Fernando committed
297 298 299
      isCode: true,
    };

300 301 302 303 304 305
    const EXPECT_RESPONSE_WITHOUT_REASON_PHRASE = {
      label: 'Actual response:',
      content: '500 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

306 307 308 309 310 311 312
    const EXPECT_RESPONSE_WITHOUT_BODY = {
      label: 'Actual response:',
      content:
        '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

313 314 315 316 317 318
    const EXPECT_RESPONSE_WITH_EMPTY_STRING = {
      label: 'Actual response:',
      content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2',
      isCode: true,
    };

Fernando's avatar
Fernando committed
319 320
    const EXPECT_RECORDED_RESPONSE = {
      label: 'Unmodified response:',
Fernando's avatar
Fernando committed
321
      content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
322 323 324
      isCode: true,
    };

325 326 327 328 329 330
    const EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE = {
      label: 'Unmodified response:',
      content: '200 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

331 332 333 334 335 336
    const EXPECT_RECORDED_RESPONSE_WITHOUT_BODY = {
      label: 'Unmodified response:',
      content: '200 OK\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

337 338 339 340 341 342
    const EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING = {
      label: 'Unmodified response:',
      content: '200 OK\nName1: Value1\nName2: Value2',
      isCode: true,
    };

343 344 345 346
    const getTextContent = (el) => el.textContent.trim();
    const getLabel = (el) => getTextContent(getByTestId(el, 'label'));
    const getContent = (el) => getTextContent(getByTestId(el, 'value'));
    const getSectionData = (testId) => {
347 348 349 350 351 352
      const section = getById(testId).element;

      if (!section) {
        return null;
      }

353
      return getAllByRole(section, 'listitem').map((li) => ({
354 355 356 357 358 359 360
        label: getLabel(li),
        content: getContent(li),
        ...(li.querySelector('code') ? { isCode: true } : {}),
      }));
    };

    it.each`
Fernando's avatar
Fernando committed
361 362 363 364 365 366 367 368
      request                                                                                             | expectedData
      ${null}                                                                                             | ${null}
      ${{}}                                                                                               | ${null}
      ${{ headers: TEST_HEADERS }}                                                                        | ${null}
      ${{ method: 'GET' }}                                                                                | ${null}
      ${{ method: 'GET', url: 'http://www.gitlab.com' }}                                                  | ${null}
      ${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }}                        | ${null}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]}
369 370 371
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: null }}               | ${[EXPECT_REQUEST_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: undefined }}          | ${[EXPECT_REQUEST_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '' }}                 | ${[EXPECT_REQUEST_WITH_EMPTY_STRING]}
372 373 374 375 376 377
    `('shows request data for $request', ({ request, expectedData }) => {
      createWrapper({ request });
      expect(getSectionData('request')).toEqual(expectedData);
    });

    it.each`
378 379 380 381 382
      response                                                                                                         | expectedData
      ${null}                                                                                                          | ${null}
      ${{}}                                                                                                            | ${null}
      ${{ headers: TEST_HEADERS }}                                                                                     | ${null}
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }}                                                           | ${null}
383
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500' }}                                        | ${[EXPECT_RESPONSE_WITHOUT_REASON_PHRASE]}
384
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]}
385 386 387
      ${{ headers: TEST_HEADERS, body: null, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}               | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, body: undefined, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}          | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, body: '', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}                 | ${[EXPECT_RESPONSE_WITH_EMPTY_STRING]}
388 389 390 391
    `('shows response data for $response', ({ response, expectedData }) => {
      createWrapper({ response });
      expect(getSectionData('response')).toEqual(expectedData);
    });
Fernando's avatar
Fernando committed
392 393

    it.each`
394 395 396 397 398 399 400 401 402
      supportingMessages                                                                                                                                         | expectedData
      ${null}                                                                                                                                                    | ${null}
      ${[]}                                                                                                                                                      | ${null}
      ${[{}]}                                                                                                                                                    | ${null}
      ${[{}, { response: {} }]}                                                                                                                                  | ${null}
      ${[{}, { response: { headers: TEST_HEADERS } }]}                                                                                                           | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]' } }]}                                                                                 | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]}                                                             | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]}                                        | ${null}
403
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200' } }]}                     | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE]}
404
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]}
405 406 407
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: null, statusCode: '200', reasonPhrase: 'OK' } }]}               | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: undefined, statusCode: '200', reasonPhrase: 'OK' } }]}          | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '', statusCode: '200', reasonPhrase: 'OK' } }]}                 | ${[EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING]}
408 409
    `('shows response data for $supporting_messages', ({ supportingMessages, expectedData }) => {
      createWrapper({ supportingMessages });
Fernando's avatar
Fernando committed
410 411
      expect(getSectionData('recorded-response')).toEqual(expectedData);
    });
412
  });
413
});