Commit e206cb86 authored by Tetiana Chupryna's avatar Tetiana Chupryna Committed by Sean McGivern

Add dependency path parsing

Issue https://gitlab.com/gitlab-org/gitlab/-/issues/229472

We add a list of ancestors in shortest path of dependencies
For all vulnerable dependencies. We don't parse this path
for all items because everything is parsed on the fly and
for large projects it could be resource concuming.
parent 3f25ac70
---
title: Parse dependency path, and present it to the frontend
merge_request: 42596
author:
type: added
...@@ -13,7 +13,8 @@ module Gitlab ...@@ -13,7 +13,8 @@ module Gitlab
def format(dependency, package_manager, file_path, vulnerabilities = []) def format(dependency, package_manager, file_path, vulnerabilities = [])
{ {
name: dependency['package']['name'], name: dependency['package']['name'],
iid: dependency['iid'],
packager: packager(package_manager), packager: packager(package_manager),
package_manager: package_manager, package_manager: package_manager,
location: formatted_location(dependency, file_path), location: formatted_location(dependency, file_path),
...@@ -59,23 +60,20 @@ module Gitlab ...@@ -59,23 +60,20 @@ module Gitlab
} }
return base_location if Feature.disabled?(:path_to_vulnerable_dependency, project) return base_location if Feature.disabled?(:path_to_vulnerable_dependency, project)
return base_location unless dependency['iid']
# TODO: update this code before https://gitlab.com/gitlab-org/gitlab/-/issues/229472 is closed
# We temporary return test dependency path to get a PoC with integration to frontend
base_location.merge({ base_location.merge({
ancestors: ancestors: formatted_dependency_path(dependency['dependency_path']),
[{ top_level: !!dependency['direct']
name: 'dep1',
version: '1.2'
},
{
name: 'dep2',
version: '10.11'
}],
top_level: false
}) })
end end
def formatted_dependency_path(dependency_path)
return unless dependency_path
dependency_path.map { |path| path.with_indifferent_access }
end
# we know that Parsers::Security::DependencyList parses one vulnerability at a time # we know that Parsers::Security::DependencyList parses one vulnerability at a time
# however, to keep interface compability with rest of the code and have MVC we return array # however, to keep interface compability with rest of the code and have MVC we return array
# even tough we know that array's size will be 1 # even tough we know that array's size will be 1
......
...@@ -5,10 +5,11 @@ module Gitlab ...@@ -5,10 +5,11 @@ module Gitlab
module Reports module Reports
module DependencyList module DependencyList
class Dependency class Dependency
attr_reader :name, :packager, :package_manager, :location, :version, :licenses, :vulnerabilities attr_reader :name, :iid, :packager, :package_manager, :location, :version, :licenses, :vulnerabilities
def initialize(params = {}) def initialize(params = {})
@name = params.fetch(:name) @name = params.fetch(:name)
@iid = params.fetch(:iid, nil)
@packager = params.fetch(:packager) @packager = params.fetch(:packager)
@package_manager = params.fetch(:package_manager) @package_manager = params.fetch(:package_manager)
@location = params.fetch(:location) @location = params.fetch(:location)
......
...@@ -5,17 +5,27 @@ module Gitlab ...@@ -5,17 +5,27 @@ module Gitlab
module Reports module Reports
module DependencyList module DependencyList
class Report class Report
# Stores dependency_path info in Hash of Hashes
# where keys of external Hash are path to dependency files
# and keys of internal Hashes are iid of dependencies
attr_reader :dependency_map
def initialize def initialize
@dependencies = {} @dependencies = {}
@dependency_map = {}
end end
def dependencies def dependencies
augment_ancestors!
@dependencies.values.map(&:to_hash) @dependencies.values.map(&:to_hash)
end end
def add_dependency(dependency) def add_dependency(dependency)
dep = Dependency.new(dependency) dep = Dependency.new(dependency)
key = dep.composite_key key = dep.composite_key
store_dependency_path_info(dep) if dep.iid
if @dependencies.has_key?(key) if @dependencies.has_key?(key)
existing_dependency = @dependencies[key] existing_dependency = @dependencies[key]
existing_dependency.update_dependency(dependency) existing_dependency.update_dependency(dependency)
...@@ -36,6 +46,36 @@ module Gitlab ...@@ -36,6 +46,36 @@ module Gitlab
def dependencies_with_licenses def dependencies_with_licenses
dependencies.select { |dependency| dependency[:licenses].any? } dependencies.select { |dependency| dependency[:licenses].any? }
end end
private
def augment_ancestors!
@dependencies.each_value do |dep|
next unless dep.iid
next if dep.location[:top_level]
if dep.vulnerabilities.empty?
dep.location.except!(:ancestors)
else
dependency_file = dep.location[:path]
dependencies_by_iid = dependency_map[dependency_file]
dep.location[:ancestors].map! do |ancestor|
next ancestor unless ancestor.fetch(:iid, false)
dependencies_by_iid[ancestor[:iid]]
end
end
end
end
def store_dependency_path_info(dependency)
dependency_file = dependency.location[:path]
dependency_map[dependency_file] ||= {}
dependency_map[dependency_file][dependency.iid] = { name: dependency.name, version: dependency.version }
end
end end
end end
end end
......
...@@ -42,6 +42,7 @@ FactoryBot.define do ...@@ -42,6 +42,7 @@ FactoryBot.define do
end end
trait :indirect do trait :indirect do
iid { 42 }
location do location do
{ {
blob_path: '/some_project/path/package_file.lock', blob_path: '/some_project/path/package_file.lock',
...@@ -61,12 +62,12 @@ FactoryBot.define do ...@@ -61,12 +62,12 @@ FactoryBot.define do
end end
trait :direct do trait :direct do
iid { 24 }
location do location do
{ {
blob_path: '/some_project/path/package_file.lock', blob_path: '/some_project/path/package_file.lock',
path: 'package_file.lock', path: 'package_file.lock',
ancestors: ancestors: nil,
[],
top_level: true top_level: true
} }
end end
......
...@@ -306,115 +306,231 @@ ...@@ -306,115 +306,231 @@
"package": { "package": {
"name": "async" "name": "async"
}, },
"version": "0.2.10" "version": "0.2.10",
"iid": 1,
"direct":true
}, },
{ {
"package": { "package": {
"name": "async" "name": "async"
}, },
"version": "1.5.2" "version": "1.5.2",
"iid": 2,
"dependency_path": [
{
"iid": 7
},
{
"iid": 16
}
]
}, },
{ {
"package": { "package": {
"name": "debug" "name": "debug"
}, },
"version": "1.0.5" "version": "1.0.5",
"iid": 3,
"direct":true
}, },
{ {
"package": { "package": {
"name": "ejs" "name": "ejs"
}, },
"version": "0.8.8" "version": "0.8.8",
"iid": 4,
"direct":true
}, },
{ {
"package": { "package": {
"name": "ms" "name": "ms"
}, },
"version": "2.0.0" "version": "2.0.0",
"iid": 5,
"dependency_path": [
{
"iid": 3
},
{
"iid": 17
}
]
}, },
{ {
"package": { "package": {
"name": "node-forge" "name": "node-forge"
}, },
"version": "0.2.24" "version": "0.2.24",
"iid": 6,
"direct":true
}, },
{ {
"package": { "package": {
"name": "saml2-js" "name": "saml2-js"
}, },
"version": "1.5.0" "version": "1.5.0",
"iid": 7,
"direct":true
}, },
{ {
"package": { "package": {
"name": "sax" "name": "sax"
}, },
"version": "1.2.4" "version": "1.2.4",
"iid": 8,
"dependency_path": [
{
"iid": 12
},
{
"iid": 11
},
{
"iid": 9
}
]
}, },
{ {
"package": { "package": {
"name": "underscore" "name": "underscore"
}, },
"version": "1.9.1" "version": "1.9.1",
"iid": 9,
"dependency_path": [
{
"iid": 12
},
{
"iid": 11
}
]
}, },
{ {
"package": { "package": {
"name": "underscore" "name": "underscore"
}, },
"version": "1.6.0" "version": "1.6.0",
"iid": 10,
"direct":true
}, },
{ {
"package": { "package": {
"name": "xml-crypto" "name": "xml-crypto"
}, },
"version": "0.8.5" "version": "0.8.5",
"iid": 11,
"dependency_path": [
{
"iid": 12
}
]
}, },
{ {
"package": { "package": {
"name": "xml-encryption" "name": "xml-encryption"
}, },
"version": "0.7.4" "version": "0.7.4",
"iid": 12,
"direct":true
}, },
{ {
"package": { "package": {
"name": "xml2js" "name": "xml2js"
}, },
"version": "0.4.19" "version":"0.4.19",
"iid": 13,
"dependency_path": [
{
"iid": 7
}
]
}, },
{ {
"package": { "package": {
"name": "xmlbuilder" "name": "xmlbuilder"
}, },
"version": "2.1.0" "version": "2.1.0",
"iid": 14,
"dependency_path":[
{
"iid": 12
},
{
"iid": 11
},
{
"iid": 9
}
]
}, },
{ {
"package": { "package": {
"name": "xmlbuilder" "name": "xmlbuilder"
}, },
"version": "9.0.7" "version": "9.0.7",
"iid": 15,
"dependency_path": [
{
"iid": 12
},
{
"iid": 11
},
{
"iid": 9
}
]
}, },
{ {
"package": { "package": {
"name": "xmldom" "name": "xmldom"
}, },
"version": "0.1.19" "version": "0.1.19",
"iid": 16,
"dependency_path":
[
{
"iid": 7
}
]
}, },
{ {
"package": { "package": {
"name": "xmldom" "name": "xmldom"
}, },
"version": "0.1.27" "version": "0.1.27",
"iid": 17,
"dependency_path": [
{
"iid": 3
}
]
}, },
{ {
"package": { "package": {
"name": "xpath.js" "name": "xpath.js"
}, },
"version": "1.1.0" "version": "1.1.0",
"iid": 18,
"dependency_path": [
{
"iid": 3
},
{
"iid": 17
},
{
"iid": 5
}
]
}, },
{ {
"package": { "package": {
"name": "xpath" "name": "xpath"
}, },
"version": "0.0.5" "version": "0.0.5",
"iid": 19,
"direct":true
} }
] ]
} }
......
...@@ -25,9 +25,19 @@ RSpec.describe Gitlab::Ci::Parsers::Security::DependencyList do ...@@ -25,9 +25,19 @@ RSpec.describe Gitlab::Ci::Parsers::Security::DependencyList do
expect(report.dependencies[0][:name]).to eq('mini_portile2') expect(report.dependencies[0][:name]).to eq('mini_portile2')
expect(report.dependencies[0][:version]).to eq('2.2.0') expect(report.dependencies[0][:version]).to eq('2.2.0')
expect(report.dependencies[0][:packager]).to eq('Ruby (Bundler)') expect(report.dependencies[0][:packager]).to eq('Ruby (Bundler)')
expect(report.dependencies[12][:packager]).to eq('JavaScript (Yarn)')
expect(report.dependencies[0][:location][:path]).to eq('rails/Gemfile.lock') expect(report.dependencies[0][:location][:path]).to eq('rails/Gemfile.lock')
expect(report.dependencies[12][:name]).to eq('xml-crypto')
expect(report.dependencies[12][:version]).to eq('0.8.5')
expect(report.dependencies[12][:packager]).to eq('JavaScript (Yarn)')
expect(report.dependencies[12][:location][:blob_path]).to eq(blob_path) expect(report.dependencies[12][:location][:blob_path]).to eq(blob_path)
expect(report.dependencies[12][:location][:top_level]).to be_falsey
expect(report.dependencies[12][:location][:ancestors]).to be_nil
expect(report.dependencies[13][:name]).to eq('xml-encryption')
expect(report.dependencies[13][:version]).to eq('0.7.4')
expect(report.dependencies[13][:location][:top_level]).to be_truthy
expect(report.dependencies[13][:location][:ancestors]).to be_nil
end end
it 'merge vulnerabilities data' do it 'merge vulnerabilities data' do
......
...@@ -16,26 +16,54 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Formatters::DependencyList do ...@@ -16,26 +16,54 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Formatters::DependencyList do
end end
describe '#format' do describe '#format' do
let(:dependency) { parsed_report['dependency_files'][0]['dependencies'][0] }
let(:package_manager) { 'bundler' } let(:package_manager) { 'bundler' }
let(:file_path) { 'rails/Gemfile.lock' } let(:file_path) { 'file.path' }
let(:data) { formatter.format(dependency, package_manager, file_path) } let(:data) { formatter.format(dependency, package_manager, file_path) }
let(:blob_path) { "/#{project.full_path}/-/blob/#{sha}/rails/Gemfile.lock" } let(:blob_path) { "/#{project.full_path}/-/blob/#{sha}/file.path" }
context 'with secure dependency' do context 'with secure dependency' do
let(:dependency) { parsed_report['dependency_files'][0]['dependencies'][0] } context 'with top-level dependency' do
let(:dependency) { parsed_report['dependency_files'][1]['dependencies'][0] }
it 'formats the dependency' do
expect(data[:name]).to eq('async')
expect(data[:iid]).to eq(1)
expect(data[:location][:blob_path]).to eq(blob_path)
expect(data[:location][:path]).to eq('file.path')
expect(data[:location][:top_level]).to be_truthy
expect(data[:location][:ancestors]).to be_nil
end
end
context 'with dependency path included' do
let(:dependency) { parsed_report['dependency_files'][1]['dependencies'][4] }
it 'formats the dependency' do
expect(data[:name]).to eq('ms')
expect(data[:iid]).to eq(5)
expect(data[:location][:blob_path]).to eq(blob_path)
expect(data[:location][:path]).to eq('file.path')
expect(data[:location][:top_level]).to be_falsey
expect(data[:location][:ancestors][0][:iid]).to eq(3)
end
end
context 'without dependency path' do
let(:dependency) { parsed_report['dependency_files'][0]['dependencies'][0] }
it 'format report into a right format' do it 'formats the dependency' do
expect(data[:name]).to eq('mini_portile2') expect(data[:name]).to eq('mini_portile2')
expect(data[:packager]).to eq('Ruby (Bundler)') expect(data[:iid]).to be_nil
expect(data[:package_manager]).to eq('bundler') expect(data[:packager]).to eq('Ruby (Bundler)')
expect(data[:location][:blob_path]).to eq(blob_path) expect(data[:package_manager]).to eq('bundler')
expect(data[:location][:path]).to eq('rails/Gemfile.lock') expect(data[:location][:blob_path]).to eq(blob_path)
expect(data[:location][:top_level]).to be_falsey expect(data[:location][:path]).to eq('file.path')
expect(data[:location][:ancestors].first[:name]).to eq('dep1') expect(data[:location][:top_level]).to be_nil
expect(data[:version]).to eq('2.2.0') expect(data[:location][:ancestors]).to be_nil
expect(data[:vulnerabilities]).to be_empty expect(data[:version]).to eq('2.2.0')
expect(data[:licenses]).to be_empty expect(data[:vulnerabilities]).to be_empty
expect(data[:licenses]).to be_empty
end
end end
end end
...@@ -49,7 +77,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Formatters::DependencyList do ...@@ -49,7 +77,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Formatters::DependencyList do
it { expect(location[:top_level]).to be_nil } it { expect(location[:top_level]).to be_nil }
it { expect(location[:ancestors]).to be_nil } it { expect(location[:ancestors]).to be_nil }
it { expect(location[:path]).to eq('rails/Gemfile.lock') } it { expect(location[:path]).to eq('file.path') }
end end
context 'with vulnerable dependency' do context 'with vulnerable dependency' do
......
...@@ -3,23 +3,44 @@ ...@@ -3,23 +3,44 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::DependencyList::Dependency do RSpec.describe Gitlab::Ci::Reports::DependencyList::Dependency do
let(:dependency_nokogiri) { build(:dependency, :nokogiri, :with_vulnerabilities) } let(:dependency_nokogiri) do
{
name: 'nokogiri',
version: '1.8.0',
packager: 'Ruby (Bundler)',
package_manager: 'Ruby (Bundler)',
location: {
blob_path: '/some_project/path/package_file.lock',
path: 'package_file.lock',
ancestors: nil,
top_level: true
},
licenses: [],
vulnerabilities: [{
name: 'DDoS',
severity: 'high'
},
{
name: 'XSS vulnerability',
severity: 'low'
}]
}
end
context 'initialize' do context 'initialize' do
specify do it 'sets all required properties' do
dependency_nokogiri[:location] = { blob_path: 'path/File_21.lock', path: 'File_21.lock' }
dep = described_class.new(dependency_nokogiri) dep = described_class.new(dependency_nokogiri)
expect(dep.to_hash).to eq({ name: 'nokogiri', expect(dep.to_hash).to eq({ name: 'nokogiri',
packager: 'Ruby (Bundler)', packager: 'Ruby (Bundler)',
package_manager: 'Ruby (Bundler)', package_manager: 'Ruby (Bundler)',
location: { blob_path: 'path/File_21.lock', path: 'File_21.lock' }, location: { blob_path: '/some_project/path/package_file.lock', path: 'package_file.lock', top_level: true, ancestors: nil },
version: '1.8.0', version: '1.8.0',
licenses: [], licenses: [],
vulnerabilities: [{ name: 'DDoS', severity: 'high' }, { name: 'XSS vulnerability', severity: 'low' }] }) vulnerabilities: [{ name: 'DDoS', severity: 'high' }, { name: 'XSS vulnerability', severity: 'low' }] })
end end
specify do it 'keeps vulnerabilities that are not duplicates' do
dependency_nokogiri[:vulnerabilities] << { name: 'problem', severity: 'high' } dependency_nokogiri[:vulnerabilities] << { name: 'problem', severity: 'high' }
dep = described_class.new(dependency_nokogiri) dep = described_class.new(dependency_nokogiri)
...@@ -28,7 +49,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Dependency do ...@@ -28,7 +49,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Dependency do
{ name: 'problem', severity: 'high' }]) { name: 'problem', severity: 'high' }])
end end
specify do it 'removes vulnerability duplicates' do
dependency_nokogiri[:vulnerabilities] << { name: 'DDoS', severity: 'high' } dependency_nokogiri[:vulnerabilities] << { name: 'DDoS', severity: 'high' }
dep = described_class.new(dependency_nokogiri) dep = described_class.new(dependency_nokogiri)
......
...@@ -6,23 +6,109 @@ require 'benchmark/ips' ...@@ -6,23 +6,109 @@ require 'benchmark/ips'
RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do
let(:report) { described_class.new } let(:report) { described_class.new }
describe '#add_dependency' do describe '#dependencies' do
let(:dependency) do subject(:dependencies) { report.dependencies }
{
name: 'gitlab', context 'without dependency path information' do
packager: '', let(:dependency) { build :dependency }
package_manager: 'bundler',
location: { blob_path: '', path: 'Gemfile' }, before do
version: '0.2.10', report.add_dependency(dependency)
vulnerabilities: [], end
licenses: []
} it 'returns array of hashes' do
expect(dependencies).to be_an(Array)
expect(dependencies.first).to be_a(Hash)
end
it 'does not contain dependency path' do
expect(dependencies.first[:location][:ancestors]).to be_nil
end
end end
subject { report.add_dependency(dependency) } context 'with dependency path information' do
let(:indirect) { build :dependency, :with_vulnerabilities, iid: 32 }
let(:direct) { build :dependency, :direct, :with_vulnerabilities }
before do
indirect[:location][:top_level] = false
indirect[:location][:ancestors] = [{ iid: direct[:iid] }]
report.add_dependency(direct)
report.add_dependency(indirect)
end
it 'generates the dependency path' do
ancestors = dependencies.second[:location][:ancestors]
expect(ancestors.last).to eq({ name: direct[:name], version: direct[:version] })
end
context 'with multiple dependency files matching same package manager' do
let(:indirect_other) { build :dependency, :with_vulnerabilities, iid: 32 }
let(:direct_other) { build :dependency, :direct, :with_vulnerabilities }
before do
indirect_other[:location][:top_level] = false
indirect_other[:location][:ancestors] = [{ iid: direct[:iid] }]
indirect_other[:location][:path] = 'other_path.lock'
direct_other[:location][:path] = 'other_path.lock'
report.add_dependency(direct_other)
report.add_dependency(indirect_other)
end
it 'generates the dependency path' do
ancestors = dependencies.second[:location][:ancestors]
ancestors_other = dependencies.last[:location][:ancestors]
expect(ancestors.last).to eq({ name: direct[:name], version: direct[:version] })
expect(ancestors_other.last).to eq({ name: direct_other[:name], version: direct_other[:version] })
end
end
context 'when method is called more than once' do
it 'generates path only once' do
dependency_before = report.dependencies.last
expect(dependency_before[:name]).to eq(indirect[:name])
expect(dependency_before[:location][:ancestors].size).to eq(1)
expect(dependency_before[:location][:ancestors].last[:name]).to eq(direct[:name])
dependency_after = report.dependencies.last
expect(dependency_after[:name]).to eq(indirect[:name])
expect(dependency_after[:location][:ancestors].size).to eq(1)
expect(dependency_after[:location][:ancestors].last[:name]).to eq(direct[:name])
end
end
context 'with not vulnerable dependencies' do
let(:indirect_secure) { build :dependency, iid: 13 }
before do
indirect_secure[:location][:top_level] = false
indirect_secure[:location][:ancestors] = [{ iid: direct[:iid] }]
report.add_dependency(indirect_secure)
end
it 'augment dependency path only for vulnerable dependencies' do
vulnerable = dependencies.find { |dep| dep[:name] == indirect[:name] }
secure = dependencies.find { |dep| dep[:name] == indirect_secure[:name] }
expect(vulnerable[:location][:ancestors].last[:name]).to eq(direct[:name])
expect(secure[:location][:ancestors]).to be_nil
end
end
end
end
describe '#add_dependency' do
let(:dependency) { build :dependency }
it 'stores given dependency params in the map' do it 'stores given dependency params in the map' do
subject report.add_dependency(dependency)
expect(report.dependencies).to eq([dependency]) expect(report.dependencies).to eq([dependency])
end end
...@@ -44,7 +130,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do ...@@ -44,7 +130,7 @@ RSpec.describe Gitlab::Ci::Reports::DependencyList::Report do
expect(report.dependencies.first.dig(:vulnerabilities)).to eq(vulnerabilities) expect(report.dependencies.first.dig(:vulnerabilities)).to eq(vulnerabilities)
end end
it 'updates dependency' do it 'stores a dependency' do
dependency[:packager] = 'Ruby (Bundler)' dependency[:packager] = 'Ruby (Bundler)'
dependency[:vulnerabilities] = [{ name: 'abc', severity: 'high' }] dependency[:vulnerabilities] = [{ name: 'abc', severity: 'high' }]
......
...@@ -28,7 +28,9 @@ RSpec.describe DependencyEntity do ...@@ -28,7 +28,9 @@ RSpec.describe DependencyEntity do
project.add_developer(user) project.add_developer(user)
end end
it { is_expected.to eq(dependency.except(:package_manager)) } it 'includes license info and vulnerabilities' do
is_expected.to eq(dependency.except(:package_manager, :iid))
end
end end
context 'with reporter' do context 'with reporter' do
...@@ -37,7 +39,7 @@ RSpec.describe DependencyEntity do ...@@ -37,7 +39,7 @@ RSpec.describe DependencyEntity do
end end
it 'includes license info and not vulnerabilities' do it 'includes license info and not vulnerabilities' do
is_expected.to eq(dependency.except(:vulnerabilities, :package_manager)) is_expected.to eq(dependency.except(:vulnerabilities, :package_manager, :iid))
end end
end end
end end
...@@ -48,7 +50,7 @@ RSpec.describe DependencyEntity do ...@@ -48,7 +50,7 @@ RSpec.describe DependencyEntity do
end end
it 'does not include licenses and vulnerabilities' do it 'does not include licenses and vulnerabilities' do
is_expected.to eq(dependency.except(:vulnerabilities, :licenses, :package_manager)) is_expected.to eq(dependency.except(:vulnerabilities, :licenses, :package_manager, :iid))
end end
end end
......
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