From b67cae4f45b957151f1c9a8ab54e70c08e423dde Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:51:02 -0700 Subject: [PATCH 1/8] Create credential_phishing_nifty.com_domain_abuse.yml --- ...ential_phishing_nifty.com_domain_abuse.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 detection-rules/credential_phishing_nifty.com_domain_abuse.yml diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml new file mode 100644 index 00000000000..7227fc3c0ca --- /dev/null +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -0,0 +1,32 @@ +name: "Credential phishing: Nifty.com domain abuse" +description: "Detects emails from nifty.com where the sender's local part matches a recipient's local part or organizational SLD, which has been observed in credential harvesting campaigns" +type: "rule" +severity: "medium" +source: | + type.inbound + and sender.email.domain.root_domain == "nifty.com" + and ( + sender.email.local_part in map(recipients.to, .email.local_part) + or sender.email.local_part in $org_slds + ) + + // negate replies + and ( + length(headers.references) == 0 + or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To"))) + ) + + // and no false positives and not solicited + and ( + ( + not profile.by_sender_email().any_messages_benign + and not profile.by_sender_email().solicited + ) + ) + +attack_types: + - "Credential Phishing" +tactics_and_techniques: + - "Spoofing" +detection_methods: + - "Sender analysis" From 496a9037e36cfa9128c296160fbc2681bfa49e92 Mon Sep 17 00:00:00 2001 From: ID Generator Date: Mon, 6 Oct 2025 21:54:37 +0000 Subject: [PATCH 2/8] Auto add rule ID --- detection-rules/credential_phishing_nifty.com_domain_abuse.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index 7227fc3c0ca..9e4553dbf81 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -30,3 +30,4 @@ tactics_and_techniques: - "Spoofing" detection_methods: - "Sender analysis" +id: "370cfdac-4976-59a1-ae1f-7cd5594eb958" From e93d26b0facdb798fe7357c41be20901f44e0cd1 Mon Sep 17 00:00:00 2001 From: Alex Herold Date: Tue, 28 Oct 2025 08:02:09 -0600 Subject: [PATCH 3/8] Sync .github directory from main branch - Applied .github directory from main to peter.new.nifty.com_domain_abuse - Ensures workflows and GitHub configurations are up to date - Automated sync via script --- .github/workflows/clear-old-test-rules.yml | 6 +- .github/workflows/rule-validate.yml | 109 ++++++++++++++------- .github/workflows/update-test-rules.yml | 2 +- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/.github/workflows/clear-old-test-rules.yml b/.github/workflows/clear-old-test-rules.yml index 2758c19199d..3ee9ce96d45 100644 --- a/.github/workflows/clear-old-test-rules.yml +++ b/.github/workflows/clear-old-test-rules.yml @@ -55,10 +55,10 @@ jobs: echo "" >> message.txt cd destination - files=$(ls **/*.yml) || true + files=$(ls -- **/*.yml) || true for file in $files; do - file_pr_num=$(yq '.testing_pr' $file) + file_pr_num=$(yq '.testing_pr' "$file") in_open_pr=false IFS=',' read -ra PR_ARRAY <<< "$OPEN_PRS" @@ -70,7 +70,7 @@ jobs: echo "$file is in open PR: $in_open_pr. File PR num: $file_pr_num" if [[ "$in_open_pr" = "false" ]]; then - rm $file + rm "$file" echo "Removed $file_pr_num" >> ../message.txt fi done diff --git a/.github/workflows/rule-validate.yml b/.github/workflows/rule-validate.yml index 5766356d129..428e1c62017 100644 --- a/.github/workflows/rule-validate.yml +++ b/.github/workflows/rule-validate.yml @@ -4,7 +4,9 @@ on: push: branches: [ "main", "test-rules" ] pull_request_target: - branches: [ "**" ] + branches: + - "main" + - 'ci-testing**' workflow_dispatch: {} issue_comment: types: [ created ] @@ -38,18 +40,23 @@ jobs: - name: Get Refs id: get_head_ref + env: + GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} + STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_REF: ${{ steps.comment_branch.outputs.head_ref }} + STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_OWNER: ${{ steps.comment_branch.outputs.head_owner }} + STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_REPO: ${{ steps.comment_branch.outputs.head_repo }} run: | # Accurate for push events, merge queues, and workflow dispatch. - head_ref="${{ github.ref }}" - repo="${{ github.repository }}" + head_ref="${GITHUB_REF}" + repo="${GITHUB_REPOSITORY}" - if [[ "${{ github.event_name }}" == 'pull_request_target' ]]; then - head_ref="${{ github.head_ref }}" - repo="${{ github.event.pull_request.head.repo.full_name }}" - elif [[ "${{ github.event_name }}" == 'issue_comment' ]]; then + if [[ "${GITHUB_EVENT_NAME}" == 'pull_request_target' ]]; then + head_ref="${GITHUB_HEAD_REF}" + repo="${GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME}" + elif [[ "${GITHUB_EVENT_NAME}" == 'issue_comment' ]]; then # Rely on comment_branch to figure out the head and base - head_ref="${{ steps.comment_branch.outputs.head_ref }}" - repo="${{ steps.comment_branch.outputs.head_owner }}/${{ steps.comment_branch.outputs.head_repo }}" + head_ref="${STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_REF}" + repo="${STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_OWNER}/${STEPS_COMMENT_BRANCH_OUTPUTS_HEAD_REPO}" fi echo "##[set-output name=head_ref;]$head_ref" @@ -63,11 +70,13 @@ jobs: fetch-depth: 0 - name: Validate Branch vs. Trigerring SHA + env: + GITHUB_EVENT_PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # If this is from a pull request validate that what we checked out is the same as the PR head. # If not we'll just fail -- the workflow will be cancelled momentarily. - if [[ "${{ github.event_name }}" == 'pull_request_target' ]]; then - if [[ "${{ github.event.pull_request.head.sha }}" != "$(git rev-parse HEAD)" ]]; then + if [[ "${GITHUB_EVENT_NAME}" == 'pull_request_target' ]]; then + if [[ "${GITHUB_EVENT_PULL_REQUEST_HEAD_SHA}" != "$(git rev-parse HEAD)" ]]; then echo "Workflow is out of date with branch, cancelling" exit 1 fi @@ -75,25 +84,27 @@ jobs: - name: Get Refs id: get_base_ref + env: + STEPS_COMMENT_BRANCH_OUTPUTS_BASE_REF: ${{ steps.comment_branch.outputs.base_ref }} run: | run_all="" base_ref="" - if [[ "${{ github.event_name }}" == 'pull_request_target' ]]; then + if [[ "${GITHUB_EVENT_NAME}" == 'pull_request_target' ]]; then # Ensure we have the latest base branch ref for accurate merge-base calculation - git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} + git fetch origin "${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" # Use the merge base to avoid including changes from target branch # that happened after this PR branch was created. - base_ref="$(git merge-base HEAD origin/${{ github.base_ref }})" - elif [[ "${{ github.event_name }}" == 'push' || "${{ github.event_name }}" == 'merge_group' ]]; then + base_ref=$(git merge-base HEAD "origin/${GITHUB_BASE_REF}") + elif [[ "${GITHUB_EVENT_NAME}" == 'push' || "${GITHUB_EVENT_NAME}" == 'merge_group' ]]; then # Detect changes based on the previous commit base_ref="$(git rev-parse HEAD^)" - elif [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then + elif [[ "${GITHUB_EVENT_NAME}" == 'workflow_dispatch' ]]; then # Run on a target, so run for all rules. run_all="true" - elif [[ "${{ github.event_name }}" == 'issue_comment' ]]; then + elif [[ "${GITHUB_EVENT_NAME}" == 'issue_comment' ]]; then # Rely on comment_branch to figure out base - base_ref="${{ steps.comment_branch.outputs.base_ref }}" + base_ref="${STEPS_COMMENT_BRANCH_OUTPUTS_BASE_REF}" fi echo "##[set-output name=run_all;]$run_all" @@ -103,12 +114,22 @@ jobs: with: python-version: '3.10' + - name: Checkout script from Sublime fork main + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: sublime-security/sublime-rules + ref: main + path: sublime-rules-main + - name: Add Rule IDs as Needed & Check for Duplicates if: github.event_name != 'issue_comment' # Run before testing, just in case this could invalidate the rule itself run: | - pip install -r scripts/generate-rule-ids/requirements.txt - python scripts/generate-rule-ids/main.py + pip install -r sublime-rules-main/scripts/generate-rule-ids/requirements.txt + python sublime-rules-main/scripts/generate-rule-ids/main.py + + # Delete path to prevent interference with later steps (such as git add and commit) + rm -r sublime-rules-main - name: Validate Rules if: github.event_name != 'issue_comment' @@ -189,6 +210,8 @@ jobs: - name: Commit & Push Results, if needed if: github.event_name != 'issue_comment' id: final_basic_validation + env: + STEPS_GET_HEAD_REF_OUTPUTS_HEAD_REF: ${{ steps.get_head_ref.outputs.head_ref }} run: | rm response.txt rm bulk_validate_request.json @@ -200,12 +223,11 @@ jobs: git config user.name 'ID Generator' git config user.email 'hello@sublimesecurity.com' - git add **/*.yml + git add -- **/*.yml git commit -m "Auto add rule ID" # This will only work when running for a pull_request_target, but rather than filter we'll let this expose # any issues. - git push origin ${{ steps.get_head_ref.outputs.head_ref }} - + git push origin "${STEPS_GET_HEAD_REF_OUTPUTS_HEAD_REF}" - name: Get the head SHA id: get_head if: ${{ always() }} @@ -223,6 +245,7 @@ jobs: env: run_url: "${{ format('https://github.com/{0}/actions/runs/{1}', steps.get_head_ref.outputs.repo, github.run_id) }}" conclusion: "${{ steps.final_basic_validation.outcome == 'success' && 'success' || 'failure' }}" + HEAD_SHA: ${{ steps.get_head.outputs.HEAD }} with: debug: ${{ secrets.ACTIONS_STEP_DEBUG || false }} retries: 3 @@ -237,7 +260,7 @@ jobs: await github.rest.checks.create({ owner: context.repo.owner, repo: context.repo.repo, - head_sha: "${{ steps.get_head.outputs.HEAD }}", + head_sha: process.env.HEAD_SHA, name: "Rule Tests and ID Updated", status: "completed", conclusion: process.env.conclusion, @@ -279,12 +302,15 @@ jobs: - name: "Find updated rule IDs" id: find_ids + env: + STEPS_GET_BASE_REF_OUTPUTS_RUN_ALL: ${{ steps.get_base_ref.outputs.run_all }} + STEPS_CHANGED_FILES_OUTPUTS_DELETED_FILES: ${{ steps.changed-files.outputs.deleted_files }} run: | for file in detection-rules/*.yml; do - rule_id=$(yq '.id' $file) + rule_id=$(yq '.id' "$file") - if [[ "${{ steps.get_base_ref.outputs.run_all }}" == "true" ]]; then - altered_rule_ids=$(echo "$rule_id"" ""$altered_rule_ids") + if [[ "${STEPS_GET_BASE_REF_OUTPUTS_RUN_ALL}" == "true" ]]; then + altered_rule_ids="${rule_id} ${altered_rule_ids}" continue fi @@ -294,18 +320,18 @@ jobs: # We only need to care when rule source is changed. This will handle renames, tag changes, etc. if [[ "$new_source" != "$old_source" ]]; then echo "$file ($rule_id) has altered source" - altered_rule_ids=$(echo "$rule_id"" ""$altered_rule_ids") + altered_rule_ids="${rule_id} ${altered_rule_ids}" fi done - for file in ${{ steps.changed-files.outputs.deleted_files }}; do - rule_id=$(yq '.id' $file) + for file in ${STEPS_CHANGED_FILES_OUTPUTS_DELETED_FILES}; do + rule_id=$(yq '.id' "$file") echo "$file ($rule_id) was deleted" - altered_rule_ids=$(echo "$rule_id"" ""$altered_rule_ids") + altered_rule_ids="${rule_id} ${altered_rule_ids}" done echo "Altered Ruled IDs: [$altered_rule_ids]" - echo "##[set-output name=rule_ids;]$(echo $altered_rule_ids)" + echo "##[set-output name=rule_ids;]${altered_rule_ids}" # TODO: This doesn't solve for a modified rule_id. We could merge with any files known on 'main', but changing # a rule ID is a separate problem. @@ -329,6 +355,8 @@ jobs: uses: actions/github-script@v6 id: find_emls_to_skip if: steps.find_pr_number.outputs.result != '' + env: + ISSUE_NUMBER: ${{ steps.find_pr_number.outputs.result }} with: debug: ${{ secrets.ACTIONS_STEP_DEBUG || false }} result-encoding: string @@ -336,7 +364,7 @@ jobs: const opts = github.rest.issues.listComments.endpoint.merge({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: "${{ steps.find_pr_number.outputs.result }}", + issue_number: process.env.ISSUE_NUMBER, }) const comments = await github.paginate(opts) @@ -408,10 +436,14 @@ jobs: only_rule_ids: '${{ steps.find_ids.outputs.rule_ids }}' skip_eml_ids: '${{ steps.find_emls_to_skip.outputs.result }}' run: | - body='{"branch":"'$branch'","repo":"'$repo'","token":"'$token'","sha":"'$sha'","only_rule_ids":"'$only_rule_ids'","skip_eml_ids":"'$skip_eml_ids'"}' - echo $body - - curl -X POST $trigger_url \ + body=$(cat <<__EOF__ + {"branch":"${branch}","repo":"${repo}","token":"${token}","sha":"${sha}","only_rule_ids":"${only_rule_ids}","skip_eml_ids":"${skip_eml_ids}"} + __EOF__ + ) + + echo "$body" + + curl -X POST "$trigger_url" \ -H 'Content-Type: application/json' \ -d "$body" @@ -434,6 +466,7 @@ jobs: id: create_check env: run_url: "${{ format('https://github.com/{0}/actions/runs/{1}', steps.get_head_ref.outputs.repo, github.run_id) }}" + HEAD_SHA: ${{ steps.get_head.outputs.HEAD }} with: debug: ${{ secrets.ACTIONS_STEP_DEBUG || false }} retries: 3 @@ -448,7 +481,7 @@ jobs: const response = await github.rest.checks.create({ owner: context.repo.owner, repo: context.repo.repo, - head_sha: "${{ steps.get_head.outputs.HEAD }}", + head_sha: process.env.HEAD_SHA, name: "MQL Mimic Tests", status: "completed", conclusion: "success", diff --git a/.github/workflows/update-test-rules.yml b/.github/workflows/update-test-rules.yml index 9579cb0aeea..e2c4861cb2e 100644 --- a/.github/workflows/update-test-rules.yml +++ b/.github/workflows/update-test-rules.yml @@ -142,7 +142,7 @@ jobs: # Handle files that may not have trailing newlines # Filter out hunk headers & file paths so we just have +- lines of changed content. LINES_CHANGES_ONLY=$(echo "$DIFF_OUTPUT" | grep -E '^[+-]' | grep -v -E '^[+-]{3}' | grep -v '^\\ No newline at end of file') - NON_TESTING_SHA_CHANGES=$(echo $LINES_CHANGES_ONLY | grep -v -E '^[+-]testing_sha: [0-9a-f]+' || true) + NON_TESTING_SHA_CHANGES=$(echo "$LINES_CHANGES_ONLY" | grep -v -E '^[+-]testing_sha: [0-9a-f]+' || true) if [[ -z "$NON_TESTING_SHA_CHANGES" ]]; then echo "\tSkipping $FILE: only testing_sha field changed. Lines changed:" echo "$LINES_CHANGES_ONLY" From deed70cf322a638bbee2e8bc8e4b93ea113a5253 Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:14:37 -0800 Subject: [PATCH 4/8] Update credential_phishing_nifty.com_domain_abuse.yml --- .../credential_phishing_nifty.com_domain_abuse.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index 9e4553dbf81..b2fbadb6865 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -10,11 +10,7 @@ source: | or sender.email.local_part in $org_slds ) - // negate replies - and ( - length(headers.references) == 0 - or not any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To"))) - ) + and ml.nlu_classifier(body.current_thread.text).language != "japanese" // and no false positives and not solicited and ( From c80a4a02dcffe7056f91513374fa805d1ad01a17 Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:29:30 -0800 Subject: [PATCH 5/8] Update rule name for Nifty.com domain abuse --- detection-rules/credential_phishing_nifty.com_domain_abuse.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index b2fbadb6865..5e9f9279a61 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -1,4 +1,4 @@ -name: "Credential phishing: Nifty.com domain abuse" +name: "Credential phishing: Nifty.com platform abuse" description: "Detects emails from nifty.com where the sender's local part matches a recipient's local part or organizational SLD, which has been observed in credential harvesting campaigns" type: "rule" severity: "medium" From bce1db4513a6dfe62e98e01ab8c5980c606fc837 Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:41:29 -0800 Subject: [PATCH 6/8] Rename rule to reflect suspicious sender address --- detection-rules/credential_phishing_nifty.com_domain_abuse.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index 5e9f9279a61..40dafd73f17 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -1,4 +1,4 @@ -name: "Credential phishing: Nifty.com platform abuse" +name: "Credential phishing: Suspicious Nifty.com sender address" description: "Detects emails from nifty.com where the sender's local part matches a recipient's local part or organizational SLD, which has been observed in credential harvesting campaigns" type: "rule" severity: "medium" From 2821c63b7d539dfad3b1046d8686b22ab91e74d4 Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:55:36 -0800 Subject: [PATCH 7/8] Rename detection rule for Nifty.com abuse --- detection-rules/credential_phishing_nifty.com_domain_abuse.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index 40dafd73f17..1305d97c599 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -1,4 +1,4 @@ -name: "Credential phishing: Suspicious Nifty.com sender address" +name: "Service Abuse: Nifty.com with impersonation" description: "Detects emails from nifty.com where the sender's local part matches a recipient's local part or organizational SLD, which has been observed in credential harvesting campaigns" type: "rule" severity: "medium" From 2b48e7995f2e654abeb4cf8e445e19633d23659a Mon Sep 17 00:00:00 2001 From: Peter Djordjevic <116412909+peterdj45@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:00:28 -0800 Subject: [PATCH 8/8] Update detection-rules/credential_phishing_nifty.com_domain_abuse.yml Co-authored-by: Brandon Murphy <4827852+zoomequipd@users.noreply.github.com> --- .../credential_phishing_nifty.com_domain_abuse.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml index 1305d97c599..8769775227b 100644 --- a/detection-rules/credential_phishing_nifty.com_domain_abuse.yml +++ b/detection-rules/credential_phishing_nifty.com_domain_abuse.yml @@ -13,12 +13,8 @@ source: | and ml.nlu_classifier(body.current_thread.text).language != "japanese" // and no false positives and not solicited - and ( - ( - not profile.by_sender_email().any_messages_benign - and not profile.by_sender_email().solicited - ) - ) + and not profile.by_sender_email().any_messages_benign + and not profile.by_sender_email().solicited attack_types: - "Credential Phishing"