Skip to content

Protect security-critical code/system #151

Protect security-critical code/system

Protect security-critical code/system #151

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");
}