Commit e7507134 authored by Lukas 'Eipi' Eipert's avatar Lukas 'Eipi' Eipert Committed by Kushal Pandya

Container Scanning: Messages and description

This improves messages and descriptions of container scanning. In order
to align with backend, we remove inherit copying of attributes with
spreading and explicitly build the same structure backend provides for
us
parent e699c439
......@@ -68,6 +68,7 @@ export default {
},
[types.SET_MODAL_DATA](state, payload) {
const { vulnerability } = payload;
const { location } = vulnerability;
Vue.set(state.modal, 'title', vulnerability.name);
Vue.set(state.modal.data.description, 'value', vulnerability.description);
......@@ -81,22 +82,38 @@ export default {
'url',
vulnerability.project && vulnerability.project.full_path,
);
Vue.set(
state.modal.data.file,
'value',
vulnerability.location &&
`${vulnerability.location.file}:${vulnerability.location.start_line}`,
);
Vue.set(
state.modal.data.identifiers,
'value',
vulnerability.identifiers.length && vulnerability.identifiers,
);
Vue.set(
state.modal.data.className,
'value',
vulnerability.location && vulnerability.location.class,
);
if (location) {
const {
file,
start_line: startLine,
end_line: endLine,
image,
operating_system: namespace,
class: className,
} = location;
let lineSuffix = '';
if (startLine) {
lineSuffix += `:${startLine}`;
if (endLine && startLine !== endLine) {
lineSuffix += `-${endLine}`;
}
}
Vue.set(state.modal.data.className, 'value', className);
Vue.set(state.modal.data.file, 'value', `${file}${lineSuffix}`);
Vue.set(state.modal.data.image, 'value', image);
Vue.set(state.modal.data.namespace, 'value', namespace);
}
Vue.set(state.modal.data.severity, 'value', vulnerability.severity);
Vue.set(state.modal.data.reportType, 'value', vulnerability.report_type);
Vue.set(state.modal.data.confidence, 'value', vulnerability.confidence);
......
......@@ -28,9 +28,11 @@ export default () => ({
file: { text: s__('Vulnerability|File') },
identifiers: { text: s__('Vulnerability|Identifiers') },
severity: { text: s__('Vulnerability|Severity') },
reportType: { text: s__('Vulnerability|Report Type') },
confidence: { text: s__('Vulnerability|Confidence') },
reportType: { text: s__('Vulnerability|Report Type') },
className: { text: s__('Vulnerability|Class') },
image: { text: s__('Vulnerability|Image') },
namespace: { text: s__('Vulnerability|Namespace') },
links: { text: s__('Vulnerability|Links') },
instances: { text: s__('Vulnerability|Instances') },
},
......
......@@ -34,7 +34,5 @@ export default {
<modal-open-name :issue="issue" :status="status" />
</div>
<report-link v-if="issue.path" :issue="issue" />
</div>
</template>
......@@ -4,11 +4,11 @@ import {
parseSastIssues,
parseDependencyScanningIssues,
filterByKey,
parseSastContainer,
parseDastIssues,
getUnapprovedVulnerabilities,
findIssueIndex,
} from './utils';
import { parseSastContainer } from './utils/container_scanning';
import { visitUrl } from '~/lib/utils/url_utility';
export default {
......@@ -118,11 +118,11 @@ export default {
[types.RECEIVE_SAST_CONTAINER_REPORTS](state, reports) {
if (reports.base && reports.head) {
const headIssues = getUnapprovedVulnerabilities(
parseSastContainer(reports.head.vulnerabilities, reports.enrichData),
parseSastContainer(reports.head.vulnerabilities, reports.enrichData, reports.head.image),
reports.head.unapproved,
);
const baseIssues = getUnapprovedVulnerabilities(
parseSastContainer(reports.base.vulnerabilities, reports.enrichData),
parseSastContainer(reports.base.vulnerabilities, reports.enrichData, reports.base.image),
reports.base.unapproved,
);
const filterKey = 'vulnerability';
......@@ -135,7 +135,7 @@ export default {
Vue.set(state.sastContainer, 'isLoading', false);
} else if (reports.head && !reports.base) {
const newIssues = getUnapprovedVulnerabilities(
parseSastContainer(reports.head.vulnerabilities, reports.enrichData),
parseSastContainer(reports.head.vulnerabilities, reports.enrichData, reports.head.image),
reports.head.unapproved,
);
......@@ -265,7 +265,8 @@ export default {
Vue.set(state.modal.data.file, 'url', issue.urlPath);
Vue.set(state.modal.data.className, 'value', issue.location && issue.location.class);
Vue.set(state.modal.data.methodName, 'value', issue.location && issue.location.method);
Vue.set(state.modal.data.namespace, 'value', issue.namespace);
Vue.set(state.modal.data.image, 'value', issue.location && issue.location.image);
Vue.set(state.modal.data.namespace, 'value', issue.location && issue.location.operating_system);
if (issue.identifiers && issue.identifiers.length > 0) {
Vue.set(state.modal.data.identifiers, 'value', issue.identifiers);
......
......@@ -75,16 +75,26 @@ export default () => ({
text: s__('ciReport|Description'),
isLink: false,
},
file: {
value: null,
url: null,
text: s__('ciReport|File'),
isLink: true,
},
identifiers: {
value: [],
text: s__('ciReport|Identifiers'),
isLink: false,
},
file: {
severity: {
value: null,
url: null,
text: s__('ciReport|File'),
isLink: true,
text: s__('ciReport|Severity'),
isLink: false,
},
confidence: {
value: null,
text: s__('ciReport|Confidence'),
isLink: false,
},
className: {
value: null,
......@@ -96,19 +106,14 @@ export default () => ({
text: s__('ciReport|Method'),
isLink: false,
},
namespace: {
value: null,
text: s__('ciReport|Namespace'),
isLink: false,
},
severity: {
image: {
value: null,
text: s__('ciReport|Severity'),
text: s__('ciReport|Image'),
isLink: false,
},
confidence: {
namespace: {
value: null,
text: s__('ciReport|Confidence'),
text: s__('ciReport|Namespace'),
isLink: false,
},
links: {
......
......@@ -43,8 +43,8 @@ export const findMatchingRemediations = (remediations, vulnerability) => {
* @param {Object} vulnerability
* @param {Array} feedback
*/
function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) {
return feedback
export const enrichVulnerabilityWithfeedback = (vulnerability, feedback = []) =>
feedback
.filter(fb => fb.project_fingerprint === vulnerability.project_fingerprint)
.reduce((vuln, fb) => {
if (fb.feedback_type === 'dismissal') {
......@@ -68,7 +68,6 @@ function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) {
}
return vuln;
}, vulnerability);
}
/**
* Generates url to repository file and highlight section between start and end lines.
......@@ -198,62 +197,6 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
});
};
/**
* Parses Container Scanning results into a common format to allow to use the same Vue component.
* Container Scanning report is currently the straigh output from the underlying tool
* (clair scanner) hence the formatting happenning here.
*
* @param {Array} issues
* @param {Array} feedback
* @returns {Array}
*/
export const parseSastContainer = (issues = [], feedback = []) =>
issues.map(issue => {
const parsed = {
...issue,
category: 'container_scanning',
project_fingerprint: sha1(
`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`,
),
title: issue.vulnerability,
description: !_.isEmpty(issue.description)
? issue.description
: sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), {
namespace: issue.namespace,
vulnerability: issue.vulnerability,
}),
path: issue.namespace,
identifiers: [
{
type: 'CVE',
name: issue.vulnerability,
value: issue.vulnerability,
url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
},
],
};
// Generate solution
if (
!_.isEmpty(issue.fixedby) &&
!_.isEmpty(issue.featurename) &&
!_.isEmpty(issue.featureversion)
) {
Object.assign(parsed, {
solution: sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), {
name: issue.featurename,
version: issue.featureversion,
fixed: issue.fixedby,
}),
});
}
return {
...parsed,
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
/**
* Parses DAST into a common format to allow to use the same Vue component.
* DAST report is currently the straigh output from the underlying tool (ZAProxy)
......
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { s__, sprintf } from '~/locale';
import sha1 from 'sha1';
import _ from 'underscore';
import { enrichVulnerabilityWithfeedback } from '../utils';
/*
Container scanning mapping utils
This file contains all functions for mapping container scanning vulnerabilities
to match the representation that we are building in the backend:
https://gitlab.com/gitlab-org/gitlab-ee/blob/bbcd07475f0334/ee/lib/gitlab/ci/parsers/security/container_scanning.rb
All these function can hopefully be removed as soon as we retrieve the data from the backend.
*/
export const formatContainerScanningDescription = ({
description,
namespace,
vulnerability,
featurename,
featureversion,
}) => {
if (!_.isEmpty(description)) {
return description;
}
let generated;
if (featurename && featureversion) {
generated = `${featurename}:${featureversion}`;
} else if (featurename) {
generated = featurename;
} else {
generated = namespace;
}
return sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), {
namespace: generated,
vulnerability,
});
};
export const formatContainerScanningMessage = ({ vulnerability, featurename }) => {
if (featurename) {
return sprintf(s__('ciReport|%{vulnerability} in %{featurename}'), {
vulnerability,
featurename,
});
}
return vulnerability;
};
export const formatContainerScanningSolution = ({ fixedby, featurename, featureversion }) => {
if (!_.isEmpty(fixedby)) {
if (!_.isEmpty(featurename)) {
if (!_.isEmpty(featureversion)) {
return sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), {
name: featurename,
version: featureversion,
fixed: fixedby,
});
}
return sprintf(s__('ciReport|Upgrade %{name} to %{fixed}.'), {
name: featurename,
fixed: fixedby,
});
}
return sprintf(s__('ciReport|Upgrade to %{fixed}.'), {
fixed: fixedby,
});
}
return null;
};
export const parseContainerScanningSeverity = severity => {
if (severity === 'Defcon1') {
return SEVERITY_LEVELS.critical;
} else if (severity === 'Negligible') {
return SEVERITY_LEVELS.low;
}
return severity;
};
/**
* Parses Container Scanning results into a common format to allow to use the same Vue component.
* Container Scanning report is currently the straight output from the underlying tool
* (clair scanner) hence the formatting happening here.
*
* @param {Array} issues
* @param {Array} feedback
* @param {String} image name
* @returns {Array}
*/
export const parseSastContainer = (issues = [], feedback = [], image) =>
issues.map(issue => {
const message = formatContainerScanningMessage(issue);
/*
The following fields are copying the backend data structure, as can be found in:
https://gitlab.com/gitlab-org/gitlab-ee/blob/f8f5724bb47712df0a618ae0a447b69a6ef47c0c/ee/lib/gitlab/ci/parsers/security/container_scanning.rb#L42-72
*/
const parsed = {
category: 'container_scanning',
message,
description: formatContainerScanningDescription(issue),
cve: issue.vulnerability,
severity: parseContainerScanningSeverity(issue.severity),
confidence: SEVERITY_LEVELS.medium,
location: {
image,
operating_system: issue.namespace,
},
scanner: { id: 'clair', name: 'Clair' },
identifiers: [
{
type: 'CVE',
name: issue.vulnerability,
value: issue.vulnerability,
url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
},
],
};
const solution = formatContainerScanningSolution(issue);
if (solution) {
parsed.solution = solution;
}
if (issue.featurename) {
const dependency = {
package: {
name: issue.featurename,
},
};
if (issue.featureversion) {
dependency.version = issue.featureversion;
}
parsed.location.dependency = dependency;
}
if (issue.link) {
parsed.links = [{ url: issue.link }];
}
/*
The following properties are set only created in the frontend.
This is done for legacy reasons and they should be made obsolete,
before switching to the Backend implementation
*/
const frontendOnly = {
project_fingerprint: sha1(
`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`,
),
title: message,
vulnerability: issue.vulnerability,
};
return {
...parsed,
...frontendOnly,
...enrichVulnerabilityWithfeedback(frontendOnly, feedback),
};
});
---
title: Enrich container scanning with more data on the frontend
merge_request: 10526
author:
type: changed
......@@ -280,6 +280,18 @@ describe('vulnerabilities module mutations', () => {
);
});
it('should set the modal className', () => {
expect(state.modal.data.className.value).toEqual(vulnerability.location.class);
});
it('should set the modal image', () => {
expect(state.modal.data.image.value).toEqual(vulnerability.location.image);
});
it('should set the modal namespace', () => {
expect(state.modal.data.namespace.value).toEqual(vulnerability.location.operating_system);
});
it('should set the modal identifiers', () => {
expect(state.modal.data.identifiers.value).toEqual(vulnerability.identifiers);
});
......
......@@ -104,12 +104,6 @@ describe('Report issues', () => {
dockerReportParsed.unapproved[0].title,
);
});
it('renders namespace', () => {
expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].path);
expect(vm.$el.textContent.trim()).toContain('in');
});
});
describe('for dast issues', () => {
......
......@@ -104,12 +104,6 @@ describe('Report issue', () => {
dockerReportParsed.unapproved[0].title,
);
});
it('renders namespace', () => {
expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].path);
expect(vm.$el.textContent.trim()).toContain('in');
});
});
describe('for dast issue', () => {
......
......@@ -54,15 +54,4 @@ describe('sast container issue body', () => {
expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.title);
});
describe('path', () => {
it('renders path', () => {
vm = mountComponent(Component, {
issue: sastContainerIssue,
status,
});
expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.path);
});
});
});
......@@ -682,7 +682,13 @@ export const parsedDependencyScanningBaseStore = [
export const parsedSastContainerBaseStore = [
{
category: 'container_scanning',
message: 'CVE-2014-8130',
description: 'debian:8 is affected by CVE-2014-8130.',
cve: 'CVE-2014-8130',
severity: 'Low',
confidence: 'Medium',
location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' },
scanner: { id: 'clair', name: 'Clair' },
identifiers: [
{
name: 'CVE-2014-8130',
......@@ -691,10 +697,7 @@ export const parsedSastContainerBaseStore = [
value: 'CVE-2014-8130',
},
],
namespace: 'debian:8',
path: 'debian:8',
project_fingerprint: '20a19f706d82cec1c04d1c9a8858e89b142d602f',
severity: 'Negligible',
title: 'CVE-2014-8130',
vulnerability: 'CVE-2014-8130',
},
......@@ -716,6 +719,7 @@ export const allIssuesParsed = [
];
export const dockerReport = {
image: 'registry.example.com/example/master:1234',
unapproved: ['CVE-2017-12944', 'CVE-2017-16232'],
vulnerabilities: [
{
......@@ -737,6 +741,7 @@ export const dockerReport = {
};
export const dockerBaseReport = {
image: 'registry.example.com/example/master:1234',
unapproved: ['CVE-2017-12944', 'CVE-2014-8130'],
vulnerabilities: [
{
......@@ -759,11 +764,14 @@ export const dockerBaseReport = {
export const dockerNewIssues = [
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
title: 'CVE-2017-16232',
path: 'debian:8',
category: 'container_scanning',
message: 'CVE-2017-16232',
description: 'debian:8 is affected by CVE-2017-16232.',
cve: 'CVE-2017-16232',
severity: 'Low',
confidence: 'Medium',
location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' },
scanner: { id: 'clair', name: 'Clair' },
identifiers: [
{
type: 'CVE',
......@@ -772,19 +780,22 @@ export const dockerNewIssues = [
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232',
},
],
category: 'container_scanning',
project_fingerprint: '4e010f6d292364a42c6bb05dbd2cc788c2e5e408',
description: 'debian:8 is affected by CVE-2017-16232.',
title: 'CVE-2017-16232',
vulnerability: 'CVE-2017-16232',
},
];
export const dockerOnlyHeadParsed = [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
category: 'container_scanning',
message: 'CVE-2017-12944',
description: 'debian:8 is affected by CVE-2017-12944.',
cve: 'CVE-2017-12944',
severity: 'Medium',
title: 'CVE-2017-12944',
path: 'debian:8',
confidence: 'Medium',
location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' },
scanner: { id: 'clair', name: 'Clair' },
identifiers: [
{
type: 'CVE',
......@@ -793,16 +804,19 @@ export const dockerOnlyHeadParsed = [
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12944',
},
],
category: 'container_scanning',
project_fingerprint: '0693a82ef93c5e9d98c23a35ddcd8ed2cbd047d9',
description: 'debian:8 is affected by CVE-2017-12944.',
title: 'CVE-2017-12944',
vulnerability: 'CVE-2017-12944',
},
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
title: 'CVE-2017-16232',
path: 'debian:8',
category: 'container_scanning',
message: 'CVE-2017-16232',
description: 'debian:8 is affected by CVE-2017-16232.',
cve: 'CVE-2017-16232',
severity: 'Low',
confidence: 'Medium',
location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' },
scanner: { id: 'clair', name: 'Clair' },
identifiers: [
{
type: 'CVE',
......@@ -811,9 +825,9 @@ export const dockerOnlyHeadParsed = [
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232',
},
],
category: 'container_scanning',
project_fingerprint: '4e010f6d292364a42c6bb05dbd2cc788c2e5e408',
description: 'debian:8 is affected by CVE-2017-16232.',
title: 'CVE-2017-16232',
vulnerability: 'CVE-2017-16232',
},
];
......
......@@ -396,11 +396,12 @@ describe('security reports mutations', () => {
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
namespace: 'debian:8',
location: {
file: 'Gemfile.lock',
class: 'User',
method: 'do_something',
image: 'https://example.org/docker/example:v1.2.3',
operating_system: 'debian:8',
},
links: [
{
......@@ -434,7 +435,8 @@ describe('security reports mutations', () => {
expect(stateCopy.modal.data.file.url).toEqual(issue.urlPath);
expect(stateCopy.modal.data.className.value).toEqual(issue.location.class);
expect(stateCopy.modal.data.methodName.value).toEqual(issue.location.method);
expect(stateCopy.modal.data.namespace.value).toEqual(issue.namespace);
expect(stateCopy.modal.data.namespace.value).toEqual(issue.location.operating_system);
expect(stateCopy.modal.data.image.value).toEqual(issue.location.image);
expect(stateCopy.modal.data.identifiers.value).toEqual(issue.identifiers);
expect(stateCopy.modal.data.severity.value).toEqual(issue.severity);
expect(stateCopy.modal.data.confidence.value).toEqual(issue.confidence);
......@@ -615,7 +617,6 @@ describe('security reports mutations', () => {
describe('UPDATE_CONTAINER_SCANNING_ISSUE', () => {
it('updates issue in the new issues list', () => {
// TODO pas dast
stateCopy.sastContainer.newIssues = dockerNewIssues;
stateCopy.sastContainer.resolvedIssues = [];
const updatedIssue = {
......
......@@ -4,7 +4,6 @@ import {
findMatchingRemediations,
parseSastIssues,
parseDependencyScanningIssues,
parseSastContainer,
parseDastIssues,
filterByKey,
getUnapprovedVulnerabilities,
......@@ -12,6 +11,14 @@ import {
statusIcon,
countIssues,
} from 'ee/vue_shared/security_reports/store/utils';
import {
formatContainerScanningDescription,
formatContainerScanningMessage,
formatContainerScanningSolution,
parseContainerScanningSeverity,
parseSastContainer,
} from 'ee/vue_shared/security_reports/store/utils/container_scanning';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import {
oldSastIssues,
sastIssues,
......@@ -226,13 +233,93 @@ describe('security reports utils', () => {
});
});
describe('container scanning utils', () => {
describe('formatContainerScanningSolution', () => {
it('should return false if there is no data', () => {
expect(formatContainerScanningSolution({})).toBe(null);
});
it('should return the correct sentence', () => {
expect(formatContainerScanningSolution({ fixedby: 'v9000' })).toBe('Upgrade to v9000.');
expect(
formatContainerScanningSolution({ fixedby: 'v9000', featurename: 'Dependency' }),
).toBe('Upgrade Dependency to v9000.');
expect(
formatContainerScanningSolution({
fixedby: 'v9000',
featurename: 'Dependency',
featureversion: '1.0-beta',
}),
).toBe('Upgrade Dependency from 1.0-beta to v9000.');
});
});
describe('formatContainerScanningMessage', () => {
it('should return concatenated message if vulnerability and featurename are provided', () => {
expect(
formatContainerScanningMessage({ vulnerability: 'CVE-124', featurename: 'grep' }),
).toBe('CVE-124 in grep');
});
it('should return vulnerability if only that is provided', () => {
expect(formatContainerScanningMessage({ vulnerability: 'Foo' })).toBe('Foo');
});
});
describe('formatContainerScanningDescription', () => {
it('should return description', () => {
expect(formatContainerScanningDescription({ description: 'Foobar' })).toBe('Foobar');
});
it('should build description from available fields', () => {
const featurename = 'Dependency';
const featureversion = '1.0';
const namespace = 'debian:8';
const vulnerability = 'CVE-123';
expect(
formatContainerScanningDescription({
featurename,
featureversion,
namespace,
vulnerability,
}),
).toBe('Dependency:1.0 is affected by CVE-123.');
expect(formatContainerScanningDescription({ featurename, namespace, vulnerability })).toBe(
'Dependency is affected by CVE-123.',
);
expect(formatContainerScanningDescription({ namespace, vulnerability })).toBe(
'debian:8 is affected by CVE-123.',
);
});
});
describe('parseContainerScanningSeverity', () => {
it('should return `Critical` for `Defcon1`', () => {
expect(parseContainerScanningSeverity('Defcon1')).toBe(SEVERITY_LEVELS.critical);
});
it('should return `Low` for `Negligible`', () => {
expect(parseContainerScanningSeverity('Negligible')).toBe('Low');
});
it('should not touch other severities', () => {
expect(parseContainerScanningSeverity('oxofrmbl')).toBe('oxofrmbl');
expect(parseContainerScanningSeverity('Medium')).toBe('Medium');
expect(parseContainerScanningSeverity('High')).toBe('High');
});
});
});
describe('parseSastContainer', () => {
it('parses sast container issues', () => {
const parsed = parseSastContainer(dockerReport.vulnerabilities)[0];
const issue = dockerReport.vulnerabilities[0];
expect(parsed.title).toEqual(issue.vulnerability);
expect(parsed.path).toEqual(issue.namespace);
expect(parsed.identifiers).toEqual([
{
type: 'CVE',
......
......@@ -11756,12 +11756,18 @@ msgstr ""
msgid "Vulnerability|Identifiers"
msgstr ""
msgid "Vulnerability|Image"
msgstr ""
msgid "Vulnerability|Instances"
msgstr ""
msgid "Vulnerability|Links"
msgstr ""
msgid "Vulnerability|Namespace"
msgstr ""
msgid "Vulnerability|Project"
msgstr ""
......@@ -12416,6 +12422,9 @@ msgstr ""
msgid "ciReport|%{reportType}: Loading resulted in an error"
msgstr ""
msgid "ciReport|%{vulnerability} in %{featurename}"
msgstr ""
msgid "ciReport|(errors when loading results)"
msgstr ""
......@@ -12503,6 +12512,9 @@ msgstr ""
msgid "ciReport|Identifiers"
msgstr ""
msgid "ciReport|Image"
msgstr ""
msgid "ciReport|Implement this solution by creating a merge request"
msgstr ""
......@@ -12603,6 +12615,12 @@ msgstr ""
msgid "ciReport|Upgrade %{name} from %{version} to %{fixed}."
msgstr ""
msgid "ciReport|Upgrade %{name} to %{fixed}."
msgstr ""
msgid "ciReport|Upgrade to %{fixed}."
msgstr ""
msgid "ciReport|Used by %{packagesString}"
msgid_plural "ciReport|Used by %{packagesString}, and %{lastPackage}"
msgstr[0] ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment