22. October 2018 alex Automated Security-Updates for Drupal Image caption Security by Nick Youngson CC BY-SA 3.0 Alpha Stock Images UPDATE: we've been asked if we could put these scripts on Github, so there you go: https://github.com/1xINTERNET/drupal-security-update-scripts We all have been there: We do not need the shiniest newest features in our Drupal installation, but we need the project to run reliably and securely. For this you can choose to only run security-updates. But it might be tedious to always look at your projects, figure out their dependencies and check for updates. At the end of the day this approach does not scale, is prone to errors and you might end up forgetting it. On the other hand, having the server update itself is not a great idea because what you might be developing against is not running on the server anymore or the update, although running fine on 99% of machines, will break your machine because you have a special edge-case present. Thus the security-update will take down your production-system and all the benefits of a CI/CD-pipeline walk out of the door. But if you have a CD approach already set up for your Drupal installation, it is easy to adopt it to help you keep your project up to date and the nature of the task at hand lends itself nicely to Kubernetes-Jobs or docker-containers in general. The approach illustrated here assumes that you have a spare database and you have a place where you can re-create the docroot-folder-structure of your project. The security-update-logic itself will not need to be available via internet so no actual webserver is required. All you need for this to work is the deployable project (either the artifact running on production or a rebuild of master that is used), a database, a re-creation of the folder structure of your webserver and shell access, drush and composer for Drupal 8. The following approach shows how we go about it and explains why. We furthermore assume that the environment the script runs in is already setup to accommodate the project. (also already containing all files used during this setup, e.g. db-dumps) The following script is for updating a Drupal 8 site just because it is a bit more complicated, for Drupal 7 you can just call drush and be done with it. D8 Update-Script #!/bin/bash updateTime=$(date +%Y%m%d-%H-%M-%S) emails=("email@to.notify", "notify@me.aswell") function noUpdateFoundFunction { echo "Security-Update ran at $updateTime, no Updates found."|mail -s "No Security-Update" ${emails[@]} exit 0 } function commitErrorHandling { exitStatus=$? if [[ $exitStatus -eq 1 ]] ; then noUpdateFoundFunction else echo "Security-Update failed at $updateTime, git encountered problem."|mail -s "Failed Security-Update" ${emails[@]} exit $exitStatus fi } function handleErrors { exitStatus=$? echo "Security-Update failed at $updateTime"|mail -s "Failed Security-Update" ${emails[@]} exit $exitStatus } function isNotInBlacklist() { element=$1 blacklist=$2 for blackListedElement in $blacklist; do if [ $element == $blackListedElement ]; then return 1; # 1 is false in bash fi done return 0;# 0 is true in bash } trap handleErrors ERR echo "Deploy new Artifact" rm -fr dist mkdir dist tar xf archive.tar -C dist rm archive.tar pushd dist/web ../vendor/drush/drush/drush sql-drop -y cat /tmp/securityDB.sql |../vendor/drush/drush/drush sql-cli rm /tmp/securityDB.sql composerModuleNames=() #disabling trap because drush will throw a status-code 1 if it finds a core-update and wants to warn. #https://github.com/drush-ops/drush/issues/3749 # see "SHELL BUILTIN COMMANDS" in `man bash` or `bash -c "help set" && bash -c "help trap"` trap - ERR set +e echo "preparing to get update-message" updateMessage=$(../vendor/drush/drush/drush pm:security -n ) echo "prepare composer-packagenames" for module in $(../vendor/bin/drush pm:security --format=list --field=Name 2>/dev/null); do composerModuleNames+=("drupal/"$module) done cd .. set -e trap handleErrors ERR git checkout -b security git config user.email "security-email@company.tld" git config user.name "security" echo "Updating packages" if [[ -n ${composerModuleNames[@]} ]]; then composer update ${composerModuleNames[@]} --with-dependencies pushd web/ ../vendor/bin/drush updb -y ../vendor/bin/drush entup -y popd trap commitErrorHandling ERR git add . git commit -a -m "Security-Update ran successfully at $updateTime" echo "Security-Update successfully executed at $updateTime \n $updateMessage "|mail -s "Successful Security-Update" ${emails[@]} else noUpdateFoundFunction fi popd The script itself works on some environmental states (like where is the DB-Dump to be used coming from, bash as shell in use) but in general it tries to not assume a lot about the environment and tries to keep the state contained within the project, e.g., calling the drush-version of the project explicitly, it also commits the changes to the VCS (you can choose to push it to your remote, for easy integration into your CI/CD-pipeline). We push the commit into our remote in a later step, so our Devs can just look at the update, ensure that nothing breaks, and no vital configurations (.htaccess, .gitignore) are overwritten and then just merge it. Before we get into the details of the script, the attentive reader might have noticed that we do not specify any database to be used by Drupal. This is because we always have a specific file present that tells Drupal what DB to use. Drupal will automatically go out and include this file, if the file itself is not present it will fail hard and fast. This setup ensures that we keep as much information concerning the project in the repository (yes we commit our settings.php) and that the environmental-specific information is kept on the environment. Seeing that the environment needs to be provisioned to cater to a project anyways there is only an upside to it. Now, let us look at the script in portions. Specified Bash-Functions function noUpdateFoundFunction { echo "Security-Update ran at $updateTime, no Updates found."|mail -s "No Security-Update" ${emails[@]} exit 0 } function commitErrorHandling { exitStatus=$? if [[ $exitStatus -eq 1 ]] ; then noUpdateFoundFunction else echo "Security-Update failed at $updateTime, git encountered problem."|mail -s "Failed Security-Update" ${emails[@]} exit $exitStatus fi } function handleErrors { exitStatus=$? echo "Security-Update failed at $updateTime"|mail -s "Failed Security-Update" ${emails[@]} exit $exitStatus } function isNotInBlacklist() { element=$1 blacklist=$2 for blackListedElement in $blacklist; do if [ $element == $blackListedElement ]; then return 1; # 1 is false in bash fi done return 0;# 0 is true in bash } The functions specified above are concerned with error-handling or dispersion of specific information. Most of these functions will be encountered in when using the "trap"-utility of bash. You will also note that commitErrorHandling() is actually a special case of handleErrors() and we could do without, so you actually have full sway as to how you want to structure this. We also provide a function to check if a module is blacklisted for update(think updatelock in drush). Clean Slate echo "Deploy new Artifact" rm -fr dist mkdir dist tar xf archive.tar -C dist rm archive.tar pushd dist/web ../vendor/drush/drush/drush sql-drop -y The code above provides a clean slate. We delete the previously deployed artifact(this is not required for containerized environments (just don't use volumes)). Getting Database cat /tmp/securityDB.sql |../vendor/drush/drush/drush sql-cli rm /tmp/securityDB.sql Here we just import the database. You can also choose to stream it into the environment via ssh/kubectl/docker and a tar-pipe. Update- and Package-Informations composerModuleNames=() #disabling trap because drush will throw a status-code 1 if it finds a core-update and wants to warn. #https://github.com/drush-ops/drush/issues/3749 # see "SHELL BUILTIN COMMANDS" in `man bash` or `bash -c "help set" && bash -c "help trap"` trap - ERR set +e echo "preparing to get update-message" updateMessage=$(../vendor/drush/drush/drush pm:security -n ) echo "prepare composer-packagenames" for module in $(../vendor/bin/drush pm:security --format=list --field=Name 2>/dev/null); do composerModuleNames+=("drupal/"$module) done cd .. set -e trap handleErrors ERR Here we prepare the udpates to be usable by composer and we also get the human-readable form printed by drush, to later ship it with the email. Please also note that we disable error-handling (listening to status-code != 0) as this is common behavior in drush to return non-zero status-code upon finding a core-update. A ticket for this situation has been opened with the project (see bash-comment). Running the actual update of Drupal 8 git checkout -b security git config user.email "security-email@company.tld" git config user.name "security" echo "Updating packages" if [[ -n ${composerModuleNames[@]} ]]; then composer update ${composerModuleNames[@]} --with-dependencies pushd web/ ../vendor/bin/drush updb -y ../vendor/bin/drush entup -y popd trap commitErrorHandling ERR git add . git commit -m "Security-Update ran successfully at $updateTime" echo "Security-Update successfully executed at $updateTime \n $updateMessage "|mail -s "Successful Security-Update" ${emails[@]} else noUpdateFoundFunction fi popd Here we actually update all the packages found using drush. Thanks to the naming-convention we can just use the drush package-names and prefix them with `drupal/` . As you can see we can also choose to put the emails in directly instead of having a function do it. As long as we do not use `local` with a variables, they are globally accessible, so the success-message can easily be put into a function without having to worry about the variables. Please also note that `git add .` is required, running `git commit -a -m "...."` will ignore untracked files. With this we have a new branch on our server that can be pushed to the remote in a subsequent step. We advise to use a force-push in this case. This ensures that there are no conflicts between the old security-branch and the new one. It also removes the necessity to account for state. Because we only care about the latest security-update and we want to be able to simulate a live-deploy with the commit made. Running this code once every 24h guarantees that the security-branch is usable for merging within this timeframe and it can be cherry-picked without conflicts to the masterbranch and merged into the develop-branch (you shouldn't have any conflicts anyways as 3rd-party code is touched only). In that sense Security-Updates are nothing other than hotfixes for 3rd-party-code and can be treated as hotfixes using their git-workflow.