Commit 58b82adb authored by Mark Florian's avatar Mark Florian Committed by Olena Horal-Koretska

Show CE security MR widget for non-Ultimate users

This makes the CE security MR widget visible for Starter and Premium
users, and also Free tier users on `.com`.

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/273205, part of
https://gitlab.com/groups/gitlab-org/-/epics/4394.
parent f1bdac24
......@@ -129,7 +129,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :security_reports_docs_path do |merge_request|
help_page_path('user/application_security/sast/index.md', anchor: 'reports-json-format')
help_page_path('user/application_security/index.md', anchor: 'viewing-security-scan-information-in-merge-requests')
end
private
......
......@@ -123,6 +123,24 @@ latest versions of the scanning tools without having to do anything. There are s
with this approach, however, and there is a
[plan to resolve them](https://gitlab.com/gitlab-org/gitlab/-/issues/9725).
## Viewing security scan information in merge requests **(CORE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4393) in GitLab Core 13.5.
> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/273205) in 13.6.
> - It's [deployed behind a feature flag](../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It can be enabled or disabled for a single project.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-the-basic-security-widget). **(CORE ONLY)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
Merge requests which have run security scans let you know that the generated
reports are available to download.
![Security widget](img/security_widget_v13_6.png)
## Interacting with the vulnerabilities
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.8.
......@@ -594,3 +612,28 @@ Analyzer results are displayed in the [job logs](../../ci/pipelines/#expand-and-
[Merge Request widget](sast/index.md#overview)
or [Security Dashboard](security_dashboard/index.md).
There is [an open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/235772) in which changes to this behavior are being discussed.
### Enable or disable the basic security widget **(CORE ONLY)**
The basic security widget is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../feature_flags.md)
can opt to disable it.
To enable it:
```ruby
# For the instance
Feature.enable(:core_security_mr_widget)
# For a single project
Feature.enable(:core_security_mr_widget, Project.find(<project id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:core_security_mr_widget)
# For a single project
Feature.disable(:core_security_mr_widget, Project.find(<project id>))
```
<script>
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import GroupedMetricsReportsApp from 'ee/vue_shared/metrics_reports/grouped_metrics_reports_app.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { componentNames } from 'ee/reports/components/issue_body';
......@@ -22,7 +21,8 @@ export default {
MrWidgetGeoSecondaryNode,
MrWidgetPolicyViolation,
BlockingMergeRequestsReport,
GroupedSecurityReportsApp,
GroupedSecurityReportsApp: () =>
import('ee/vue_shared/security_reports/grouped_security_reports_app.vue'),
GroupedMetricsReportsApp,
ReportSection,
},
......@@ -88,9 +88,13 @@ export default {
return Boolean(loadPerformance.head_path && loadPerformance.base_path);
},
shouldRenderSecurityReport() {
shouldRenderBaseSecurityReport() {
return !this.mr.canReadVulnerabilities && this.shouldRenderSecurityReport;
},
shouldRenderExtendedSecurityReport() {
const { enabledReports } = this.mr;
return (
this.mr.canReadVulnerabilities &&
enabledReports &&
this.$options.securityReportTypes.some(reportType => enabledReports[reportType])
);
......@@ -306,8 +310,15 @@ export default {
:endpoint="mr.metricsReportsPath"
class="js-metrics-reports-container"
/>
<security-reports-app
v-if="shouldRenderBaseSecurityReport"
:pipeline-id="mr.pipeline.id"
:project-id="mr.targetProjectId"
:security-reports-docs-path="mr.securityReportsDocsPath"
/>
<grouped-security-reports-app
v-if="shouldRenderSecurityReport"
v-else-if="shouldRenderExtendedSecurityReport"
:head-blob-path="mr.headBlobPath"
:source-branch="mr.sourceBranch"
:target-branch="mr.targetBranch"
......
......@@ -13,6 +13,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.coverageFuzzingHelp = data.coverage_fuzzing_help_path;
this.secretScanningHelp = data.secret_scanning_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path;
this.canReadVulnerabilities = data.can_read_vulnerabilities;
this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path;
this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
......
......@@ -65,6 +65,10 @@ module EE
merge_request.head_pipeline.iid
end
expose :can_read_vulnerabilities do |merge_request|
can?(current_user, :read_vulnerability, merge_request.project)
end
expose :can_read_vulnerability_feedback do |merge_request|
can?(current_user, :read_vulnerability_feedback, merge_request.project)
end
......
---
title: Show basic security scan information in merge requests for non-Ultimate users
merge_request: 46458
author:
type: added
......@@ -19,6 +19,7 @@ import mockData, {
headBrowserPerformance,
baseLoadPerformance,
headLoadPerformance,
pipelineJobs,
} from './mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
......@@ -74,7 +75,8 @@ describe('ee merge request widget options', () => {
const findBrowserPerformanceWidget = () => vm.$el.querySelector('.js-browser-performance-widget');
const findLoadPerformanceWidget = () => vm.$el.querySelector('.js-load-performance-widget');
const findSecurityWidget = () => vm.$el.querySelector('.js-security-widget');
const findExtendedSecurityWidget = () => vm.$el.querySelector('.js-security-widget');
const findBaseSecurityWidget = () => vm.$el.querySelector('[data-testid="security-mr-widget"]');
const setBrowserPerformance = (data = {}) => {
const browserPerformance = { ...DEFAULT_BROWSER_PERFORMANCE, ...data };
......@@ -105,15 +107,18 @@ describe('ee merge request widget options', () => {
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
vm.loading = false;
});
it('should render loading indicator', () => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(SAST_SELECTOR)
.textContent.trim(),
).toContain('SAST is loading');
......@@ -131,7 +136,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -153,7 +158,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent,
).trim(),
......@@ -173,9 +178,9 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(trimText(findSecurityWidget().querySelector(SAST_SELECTOR).textContent)).toContain(
'SAST: Loading resulted in an error',
);
expect(
trimText(findExtendedSecurityWidget().querySelector(SAST_SELECTOR).textContent),
).toContain('SAST: Loading resulted in an error');
done();
});
});
......@@ -197,14 +202,19 @@ describe('ee merge request widget options', () => {
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
mock.onGet(DEPENDENCY_SCANNING_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render loading indicator', () => {
expect(
trimText(findSecurityWidget().querySelector(DEPENDENCY_SCANNING_SELECTOR).textContent),
trimText(
findExtendedSecurityWidget().querySelector(DEPENDENCY_SCANNING_SELECTOR).textContent,
),
).toContain('Dependency scanning is loading');
});
});
......@@ -221,7 +231,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -247,7 +257,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -269,7 +279,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -289,7 +299,9 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(findSecurityWidget().querySelector(DEPENDENCY_SCANNING_SELECTOR).textContent),
trimText(
findExtendedSecurityWidget().querySelector(DEPENDENCY_SCANNING_SELECTOR).textContent,
),
).toContain('Dependency scanning: Loading resulted in an error');
done();
});
......@@ -615,14 +627,19 @@ describe('ee merge request widget options', () => {
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
mock.onGet(CONTAINER_SCANNING_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render loading indicator', () => {
expect(
trimText(findSecurityWidget().querySelector(CONTAINER_SCANNING_SELECTOR).textContent),
trimText(
findExtendedSecurityWidget().querySelector(CONTAINER_SCANNING_SELECTOR).textContent,
),
).toContain('Container scanning is loading');
});
});
......@@ -639,7 +656,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${CONTAINER_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -660,7 +677,7 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(CONTAINER_SCANNING_SELECTOR)
.textContent.trim(),
).toContain('Container scanning: Loading resulted in an error');
......@@ -685,14 +702,17 @@ describe('ee merge request widget options', () => {
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
mock.onGet(DAST_ENDPOINT).reply(200, dastDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render loading indicator', () => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(DAST_SELECTOR)
.textContent.trim(),
).toContain('DAST is loading');
......@@ -710,7 +730,7 @@ describe('ee merge request widget options', () => {
it('should render provided data', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(`${DAST_SELECTOR} .report-block-list-issue-description`)
.textContent.trim(),
).toEqual('DAST detected 1 critical severity vulnerability.');
......@@ -730,7 +750,7 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(DAST_SELECTOR)
.textContent.trim(),
).toContain('DAST: Loading resulted in an error');
......@@ -769,7 +789,7 @@ describe('ee merge request widget options', () => {
vm = mountWithFeatureFlag();
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(COVERAGE_FUZZING_SELECTOR)
.textContent.trim(),
).toContain('Coverage fuzzing is loading');
......@@ -786,7 +806,7 @@ describe('ee merge request widget options', () => {
it('should render provided data', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(`${COVERAGE_FUZZING_SELECTOR} .report-block-list-issue-description`)
.textContent.trim(),
).toEqual('Coverage fuzzing detected 1 critical and 1 high severity vulnerabilities.');
......@@ -805,7 +825,7 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(COVERAGE_FUZZING_SELECTOR)
.textContent.trim(),
).toContain('Coverage fuzzing: Loading resulted in an error');
......@@ -841,7 +861,9 @@ describe('ee merge request widget options', () => {
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
expect(
trimText(findSecurityWidget().querySelector(SECRET_SCANNING_SELECTOR).textContent),
trimText(
findExtendedSecurityWidget().querySelector(SECRET_SCANNING_SELECTOR).textContent,
),
).toContain('Secret scanning is loading');
});
});
......@@ -858,7 +880,7 @@ describe('ee merge request widget options', () => {
setImmediate(() => {
expect(
trimText(
findSecurityWidget().querySelector(
findExtendedSecurityWidget().querySelector(
`${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
......@@ -879,7 +901,7 @@ describe('ee merge request widget options', () => {
it('should render error indicator', done => {
setImmediate(() => {
expect(
findSecurityWidget()
findExtendedSecurityWidget()
.querySelector(SECRET_SCANNING_SELECTOR)
.textContent.trim(),
).toContain('Secret scanning: Loading resulted in an error');
......@@ -921,6 +943,37 @@ describe('ee merge request widget options', () => {
});
});
describe('CE security report', () => {
const PIPELINE_JOBS_ENDPOINT = `/api/undefined/projects/${mockData.target_project_id}/pipelines/${mockData.pipeline.id}/jobs`;
describe.each`
context | canReadVulnerabilities | hasPipeline | featureFlag | shouldRender
${'user cannot read vulnerabilities'} | ${false} | ${true} | ${true} | ${true}
${'user can read vulnerabilities'} | ${true} | ${true} | ${true} | ${false}
${'no pipeline'} | ${false} | ${false} | ${true} | ${false}
${'the feature flag is disabled'} | ${false} | ${true} | ${false} | ${false}
`('given $context', ({ canReadVulnerabilities, hasPipeline, featureFlag, shouldRender }) => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
can_read_vulnerabilities: canReadVulnerabilities,
pipeline: hasPipeline ? mockData.pipeline : undefined,
};
gon.features = { coreSecurityMrWidget: featureFlag };
mock.onGet(PIPELINE_JOBS_ENDPOINT).replyOnce(200, pipelineJobs);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
return waitForPromises();
});
it(`${shouldRender ? 'renders' : 'does not render'} the CE security report`, () => {
expect(findBaseSecurityWidget()).toEqual(shouldRender ? expect.any(HTMLElement) : null);
});
});
});
describe('computed', () => {
describe('shouldRenderApprovals', () => {
it('should return false when in empty state', () => {
......@@ -1097,8 +1150,26 @@ describe('ee merge request widget options', () => {
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
expect(findSecurityWidget()).toBe(null);
expect(findExtendedSecurityWidget()).toBe(null);
});
});
});
describe('given the user cannot read vulnerabilites', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
can_read_vulnerabilities: false,
enabled_reports: {
sast: true,
},
};
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('does not render the EE security report', () => {
expect(findExtendedSecurityWidget()).toBe(null);
});
});
});
......@@ -2,6 +2,7 @@ import mockData, { mockStore } from 'jest/vue_mr_widget/mock_data';
export default {
...mockData,
can_read_vulnerabilities: true,
vulnerability_feedback_help_path: '/help/user/application_security/index',
enabled_reports: {
sast: false,
......@@ -126,3 +127,14 @@ export const codequalityParsedIssues = [
];
export { mockStore };
// TODO: Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/249544
export const pipelineJobs = [
{
artifacts: [
{
file_type: 'sast',
},
],
},
];
......@@ -246,6 +246,28 @@ RSpec.describe MergeRequestWidgetEntity do
expect(subject.as_json).to include(:create_vulnerability_feedback_dismissal_path)
end
describe '#can_read_vulnerabilities' do
context 'when security dashboard feature is available' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'is set to true' do
expect(subject.as_json[:can_read_vulnerabilities]).to eq(true)
end
end
context 'when security dashboard feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'is set to false' do
expect(subject.as_json[:can_read_vulnerabilities]).to eq(false)
end
end
end
describe '#can_read_vulnerability_feedback' do
context 'when user has permissions to read vulnerability feedback' do
before do
......
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