Commit 7a677fe8 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '5528-container-scanning-fe' into 'master'

Enrich container scanning with more data on the frontend

Closes #5528

See merge request gitlab-org/gitlab-ee!10526
parents e699c439 e7507134
......@@ -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