Protect security-critical code/system #151
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Protect security-critical code/system | |
# - ensures that the systems in place to guarantee audits, approvals, versioning, test coverage etc. cannot be easily deactivated | |
# or altered without approval of the Information Security Manager (or CTO) | |
# - protects any git actions in the folder .github/workflows/* | |
# - protects the pre-commit checker script stored in .husky/pre-commit | |
on: | |
pull_request_review: | |
types: [submitted] | |
jobs: | |
protect-critical-code: | |
if: ${{ github.event.pull_request.draft == false }} | |
runs-on: ubuntu-latest | |
permissions: | |
pull-requests: write | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4.1.7 | |
with: | |
fetch-depth: 0 ##### Fetch all history for all branches | |
- name: Check Git Diff for protected files | |
id: check_protected_files | |
run: | | |
##### get all files modified by this PR | |
FILES="$(git diff --name-only origin/main HEAD)" | |
##### make sure that there are modified files | |
if [[ -z $FILES ]]; then | |
echo -e "\033[31mNo files found. This should not happen. Please check the code of the Github action. Aborting now.\033[0m" | |
echo "CONTINUE=false" >> $GITHUB_ENV | |
exit 1 | |
fi | |
##### Initialize empty variables | |
PROTECTED_FILES="" | |
##### go through all modified file names/paths and identify contracts with path '.github/' | |
while IFS= read -r FILE; do | |
# Validate file exists | |
if [[ ! -f "$FILE" ]]; then | |
echo "Warning: File $FILE not found" | |
continue | |
fi | |
##### check for github actions and pre-commit checker paths | |
if echo "$FILE" | grep -iE '^\.github/|^\.husky/pre-commit'; then ##### modified git action found | |
PROTECTED_FILES="${PROTECTED_FILES}${FILE}"$'\n' | |
fi | |
done <<< "$FILES" | |
##### if none found, exit here as there is nothing to do | |
if [[ -z "$PROTECTED_FILES" ]]; then | |
echo -e "\033[32mThis PR does not change any security-relevant code.\033[0m" | |
echo -e "\033[32mNo further checks are required.\033[0m" | |
# set action output to false | |
echo "CONTINUE=false" >> $GITHUB_ENV | |
exit 0 | |
else | |
##### set action output to true | |
echo -e "\033[31mThe following security-relevant files were are changed by this PR:\033[0m" | |
echo "$PROTECTED_FILES" | |
echo "CONTINUE=true" >> $GITHUB_ENV | |
fi | |
- name: Get "Information Security Manager" Group Members | |
if: env.CONTINUE == 'true' | |
env: | |
GH_PAT: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
##### unset the default git token (does not have sufficient rights to get team members) | |
unset GITHUB_TOKEN | |
##### use the Personal Access Token to log into git CLI | |
gh auth login --with-token < <(echo "$GH_PAT") || { echo "Failed to login with GitHub CLI"; exit 1; } | |
##### Function to get team members using github CLI | |
getTeamMembers() { | |
local org=$1 | |
local team=$2 | |
gh api \ | |
-H "Accept: application/vnd.github+json" \ | |
-H "X-GitHub-Api-Version: 2022-11-28" \ | |
"/orgs/$org/teams/$team/members" | jq -r '.[].login' | |
} | |
ORG_NAME='lifinance' | |
GROUP_NAME='InformationSecurityManager' | |
##### get team members | |
INFORMATION_SECURITY_MEMBERS=$(getTeamMembers $ORG_NAME $GROUP_NAME) | |
echo "Team members of 'Information Security Manager' group: $INFORMATION_SECURITY_MEMBERS" | |
##### store members in variable | |
echo -e "$INFORMATION_SECURITY_MEMBERS" > itSec_git_handles.txt | |
- name: Check approval of Information Security Manager | |
id: check-sec-mgr-approval | |
uses: actions/github-script@v7 | |
if: env.CONTINUE == 'true' | |
env: | |
PR_NUMBER: ${{ github.event.pull_request.number || github.event.review.pull_request.number }} | |
with: | |
script: | | |
const fs = require('fs'); | |
// ANSI escape codes for colors (used for colored output in Git action console) | |
const colors = { | |
reset: "\033[0m", | |
red: "\033[31m", | |
green: "\033[32m", | |
yellow: "\033[33m", | |
}; | |
// Read git handles from file | |
const itSecHandlesFile = 'itSec_git_handles.txt'; | |
const itSecHandles = fs.readFileSync(itSecHandlesFile, 'utf-8').split(/\r?\n/).filter(Boolean); | |
if (!itSecHandles) { | |
console.log(`${colors.red}Could not get the git handles of the InformationSecurityManager team.${colors.reset}`); | |
core.setFailed("Cannot read from InformationSecurityManager team."); | |
return; | |
} | |
const pullNumber = process.env.PR_NUMBER; | |
if (!pullNumber) { | |
console.log(`${colors.red}No PR number found in context.${colors.reset}`); | |
core.setFailed("PR number is missing."); | |
return; | |
} | |
// get all reviewers that have approved this PR | |
const { data: reviews } = await github.rest.pulls.listReviews({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
pull_number: pullNumber, | |
}); | |
// make sure that reviews are available | |
if(!reviews || reviews.length === 0) { | |
console.log(`${colors.red}Could not get reviewers of this PR from Github. Are there any reviews yet?${colors.reset}`); | |
console.log(`${colors.red}Check failed.${colors.reset}`); | |
core.setFailed("Required approval is missing"); | |
return | |
} | |
// Filter to only include reviews that have "APPROVED" status | |
const approvedReviews = reviews.filter(review => review.state === 'APPROVED'); | |
if(!approvedReviews.length) { | |
console.log(`${colors.red}Could not find any reviews with approval.${colors.reset}`); | |
console.log(`${colors.red}Cannot continue. Check failed.${colors.reset}`); | |
core.setFailed("Required approval is missing"); | |
return | |
} | |
// extract the git login handles of all reviewers that approved this PR | |
const reviewerHandles = approvedReviews.map(review => review.user.login); | |
if(approvedReviews.length === 0) | |
console.log(`${colors.red}This PR has no approvals${colors.reset}`); | |
else | |
console.log(`This PR has been approved by the following git members: ${reviewerHandles}`); | |
// check if at least one of these reviewers is member of InformationSecurityManager group | |
if (reviewerHandles.some((handle) => itSecHandles.includes(handle))) { | |
console.log(`${colors.green}The current PR is approved by a member of the InformationSecurityManager group.${colors.reset}`); | |
console.log(`${colors.green}Check passed.${colors.reset}`); | |
core.setOutput('approved', 'true'); | |
} else { | |
console.log(`${colors.red}The PR requires a missing approval by a member of the InformationSecurityManager group (https://github.com/orgs/lifinance/teams/InformationSecurityManager).${colors.reset}`); | |
console.log(`${colors.red}Check failed.${colors.reset}`); | |
core.setFailed("Required approval is missing"); | |
} |