summaryrefslogtreecommitdiff
path: root/scripts/security-harness
blob: ec062fc17cc9f35151d1a94f74a425c5086434ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env ruby

# frozen_string_literal: true

require 'digest'
require 'fileutils'
require 'open3'

if ENV['NO_COLOR']
  SHELL_RED    = ''
  SHELL_GREEN  = ''
  SHELL_YELLOW = ''
  SHELL_CLEAR  = ''
else
  SHELL_RED    = "\e[1;31m"
  SHELL_GREEN  = "\e[1;32m"
  SHELL_YELLOW = "\e[1;33m"
  SHELL_CLEAR  = "\e[0m"
end

LEFTHOOK_GLOBAL_CONFIG_PATH = File.expand_path("../lefthook.yml", __dir__)
HOOK_PATH = Open3.capture3("git rev-parse --path-format=absolute --git-path hooks/pre-push")[0].strip
HOOK_DATA = <<~HOOK
  #!/usr/bin/env bash

  set -e

  url="$2"

  if [[ "$url" != *"gitlab-org/security/"* ]]
  then
    echo "Pushing to remotes other than gitlab.com/gitlab-org/security has been disabled!"
    echo "Run scripts/security-harness to disable this check."
    echo

    exit 1
  fi
HOOK

def hook_exist?
  File.exist?(HOOK_PATH)
end

def lefthook_hook_in_place?
  hook_exist? && File.foreach(HOOK_PATH).grep(/lefthook/i).any?
end

def lefthook_available?
  system('bundle exec lefthook run prepare-commit-msg &>/dev/null') # rubocop:disable GitlabSecurity/SystemCommandInjection
end

def uninstall_lefthook
  return unless lefthook_available?

  system('bundle exec lefthook uninstall') # rubocop:disable GitlabSecurity/SystemCommandInjection
  # `bundle exec lefthook uninstall` removes the `lefthook.yml` file so we checkout it again
  system("git checkout -- #{LEFTHOOK_GLOBAL_CONFIG_PATH}") # rubocop:disable GitlabSecurity/SystemCommandInjection
  puts "#{SHELL_YELLOW}Lefthook was uninstalled to let the security harness work properly.#{SHELL_CLEAR}"
end

def install_lefthook
  return unless lefthook_available?

  system('bundle exec lefthook install') # rubocop:disable GitlabSecurity/SystemCommandInjection
  puts "#{SHELL_GREEN}Lefthook was re-installed.#{SHELL_CLEAR}"
end

def write_hook
  FileUtils.mkdir_p(File.dirname(HOOK_PATH))
  File.open(HOOK_PATH, 'w') do |file|
    file.write(HOOK_DATA)
  end
  File.chmod(0755, HOOK_PATH)
  puts "#{SHELL_GREEN}Security harness installed -- you will only be able to push to gitlab.com/gitlab-org/security!#{SHELL_CLEAR}"
end

def delete_hook
  FileUtils.rm(HOOK_PATH)
  puts "#{SHELL_YELLOW}Security harness removed -- you can now push to all remotes.#{SHELL_CLEAR}"
end

def hook_file_sum
  Digest::SHA256.file(HOOK_PATH).hexdigest
end

def hook_data_sum
  Digest::SHA256.hexdigest(HOOK_DATA)
end

# If we were to change the script and then check for a pre-existing hook before
# writing, the check would fail even if the user had an unmodified version of
# the old hook. Checking previous version hashes allows us to safely overwrite a
# script that differs from the current version, as long as it's an old one and
# not custom.
def upgrade_available?
  # SHA256 hashes of previous iterations of the script contained in `HOOK_DATA`
  %w[
    010bf0363a911ebab2bd5728d80795ed02388da51815f0b2530d08ae8ac574f0
    d9866fc672f373d631eed9cd8dc9c920fa3d36ff26d956fb96a4082a0931b371
  ].include?(hook_file_sum)
end

def current_version?
  hook_data_sum == hook_file_sum
end

# Uninstall Lefthook if it's in place
uninstall_lefthook if lefthook_hook_in_place?

if hook_exist?
  # Deal with a pre-existing hook
  if upgrade_available?
    # Upgrading from a previous version, update in-place
    write_hook
  elsif current_version?
    # Delete the hook if we're already using the current version
    delete_hook

    # Re-install Lefthook pre-push hook
    install_lefthook
  else
    # Pre-existing hook we didn't create; do nothing
    puts "#{SHELL_RED}#{HOOK_PATH} exists and is different from our hook!"
    puts "Remove it and re-run this script to continue.#{SHELL_CLEAR}"

    exit 1
  end
else
  write_hook
end