diff --git a/.github/workflows/manual-backport.yml b/.github/workflows/manual-backport.yml new file mode 100644 index 000000000..1d57a29e4 --- /dev/null +++ b/.github/workflows/manual-backport.yml @@ -0,0 +1,78 @@ +name: manual-backport +on: + workflow_dispatch: + inputs: + target_branch: + description: 'Branch to backport to (e.g. 8.0)' + required: true + commit_sha: + description: 'Sha256 hash of the merge commit to use in backporting' + required: true + exceptions: + description: 'Comma seperated list of files to skip staging e.g. detection_rules/etc/packages.yml,detection_rules/attack.py)' + required: false + +jobs: + commit: + runs-on: ubuntu-latest + + steps: + + - name: Checkout detection-rules + uses: actions/checkout@v2 + with: + token: ${{ secrets.PROTECTIONS_MACHINE_TOKEN }} + ref: main + + - name: Set github config + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Get branch histories + run: | + git fetch origin main --depth 100 + git fetch origin "${{github.event.inputs.target_branch}}" --depth 1 + git status + git log -1 --format='%H' + + - name: Checkout the commit into the staging area + run: | + # Checkout the merged commit + git checkout ${{github.event.inputs.commit_sha}} + + # Move it to the staging area + git reset --soft HEAD^ + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + + - name: Prune non-"${{github.event.inputs.target_branch}}" rules + env: + UNSTAGED_LIST_FILE: "../unstaged-rules.txt" + run: | + python -m detection_rules dev unstage-incompatible-rules --target-stack-version "${{github.event.inputs.target_branch}}" --exception-list "${{github.event.inputs.exceptions}}" + + # Track which rules were unstaged + git diff --name-only > $UNSTAGED_LIST_FILE + + # Since they've been tracked, remove any untracked files + git checkout -- . + + - name: Commit and push to "${{github.event.inputs.target_branch}}" + env: + COMMIT_MSG_FILE: "../commit-message.txt" + UNSTAGED_LIST_FILE: "../unstaged-rules.txt" + TARGET_BRANCH: "${{github.event.inputs.target_branch}}" + COMMIT_SHA: "${{github.event.inputs.commit_sha}}" + run: | + ./detection_rules/etc/commit-and-push.sh $TARGET_BRANCH $COMMIT_SHA + + - name: "Notify slack on failure" + uses: craftech-io/slack-action@v1 + with: + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + status: failure + if: failure() diff --git a/detection_rules/devtools.py b/detection_rules/devtools.py index 9353de4ee..54eadd542 100644 --- a/detection_rules/devtools.py +++ b/detection_rules/devtools.py @@ -196,11 +196,13 @@ class GitChangeEntry: @dev_group.command("unstage-incompatible-rules") @click.option("--target-stack-version", "-t", help="Minimum stack version to filter the staging area", required=True) @click.option("--dry-run", is_flag=True, help="List the changes that would be made") -def prune_staging_area(target_stack_version: str, dry_run: bool): +@click.option("--exception-list", help="List of files to skip staging", default="") +def prune_staging_area(target_stack_version: str, dry_run: bool, exception_list: list): """Prune the git staging area to remove changes to incompatible rules.""" exceptions = { "detection_rules/etc/packages.yml", } + exceptions.update(exception_list.split(",")) target_stack_version = Version(target_stack_version)[:2] diff --git a/detection_rules/etc/commit-and-push.sh b/detection_rules/etc/commit-and-push.sh new file mode 100755 index 000000000..fb5180692 --- /dev/null +++ b/detection_rules/etc/commit-and-push.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -x +set -e + +echo "Switch to the target branch and keep the staged changes" +TARGET_BRANCH=$1 +COMMIT_SHA=$2 +echo "Backporting from commit ${COMMIT_SHA} on branch ${TARGET_BRANCH}" + +echo "Stashing changes" +git stash + +echo "Checking out target branch" +git checkout ${TARGET_BRANCH} + +echo "Applying new changes" +(git stash apply --quiet) || true + +echo "Selecting incoming changes to be committed" +git checkout --theirs . --quiet + +echo "Track new changes" +git add -A + +NEEDS_BACKPORT=$(git diff HEAD --quiet --exit-code && echo n || echo y) + +if [ "n" = "$NEEDS_BACKPORT" ] +then +echo "No changes to backport" +exit 0 +fi + +echo "Create the new commit with the same author" +git commit --reuse-message ${COMMIT_SHA} + +echo "Save the commit message" +git log ${COMMIT_SHA} --format=%B -n1 > $COMMIT_MSG_FILE + +echo "Append to the commit message" +if [ -s "$UNSTAGED_LIST_FILE" ] +then +echo "Track note for the removed files" + +echo "" >> $COMMIT_MSG_FILE +echo "Removed changes from:" >> $COMMIT_MSG_FILE +awk '{print "- " $0}' $UNSTAGED_LIST_FILE >> $COMMIT_MSG_FILE +echo "" >> $COMMIT_MSG_FILE +echo '(selectively cherry picked from commit ${COMMIT_SHA})' >> $COMMIT_MSG_FILE +else +echo "No removed files" + +echo "" >> $COMMIT_MSG_FILE +echo '(cherry picked from commit ${COMMIT_SHA})' >> $COMMIT_MSG_FILE +fi + +echo "Amend the commit message and push" +git commit --amend -F $COMMIT_MSG_FILE +git push