allthingslinux / tux

Tux is an all in one bot for the All Things Linux discord server.
https://discord.gg/linux
GNU General Public License v3.0
79 stars 22 forks source link

rm -rf automod regex #495

Open tuxgitbot[bot] opened 2 weeks ago

tuxgitbot[bot] commented 2 weeks ago

to discuss proper automod regex for blocking rm rf jokes properly

let's move from the tux automod to discord native automod so messages are not sent at all

kzndotsh commented 2 weeks ago

https://support.discord.com/hc/en-us/articles/10069840290711-Filter-Messages-Using-Regular-Expressions-Regex

kzndotsh commented 2 weeks ago

We use the Rust flavor of regex and recommend writing your regex in Rust syntax to minimize errors. To edit and test your regex syntax in Rust, we recommend using Rustexp. Case-insensitive and unicode support flags are on by default for every regex pattern.

kzndotsh commented 2 weeks ago

https://treeben77.github.io/automod-regex-generator/

kzndotsh commented 2 weeks ago

Automod resources

necoarc0 commented 2 weeks ago

here is a prototype regex (?i)\brm\s(-{1,2}(r|f|fr|rf|recursive|force)\s){1,2}\b|\b(r\sm|m\sr)\s(-{1,2}\w+\s){1,2}\b

kzndotsh commented 2 weeks ago

https://github.com/allthingslinux/tux/blob/43ddda891d5d02bea8f8f152644d3d0a456321b8/tux/utils/functions.py#L7

necoarc0 commented 2 weeks ago

what do you think about @kzndotsh

necoarc0 commented 2 weeks ago

it need also image

kzndotsh commented 2 weeks ago

what do you think about @kzndotsh

check if it works with the rustexp lib they use

kzndotsh commented 2 weeks ago

1. Variations and Misspellings:

People might try to bypass your filter by purposefully adding typos or slight variations. You should account for common misspellings and variations like:

Example:

\s*s(u|U)(d|D)(o|O)\s*r\s*-{0,2}r(\s*|\s*-){0,2}f\s*

2. Contextual Use:

You should ensure that the context in which these terms are used is also considered, as you don't want to block legitimate discussions around these commands.

Example:

\b(s(u|U)(d|D)(o|O)\s+r\s+-r-{0,2}f)\b

3. Space and Separator Variations:

People might use multiple spaces or different types of separators (like underscores, dashes) to bypass simple regex.

Example:

\s*s(\\_?\s*u_?\s*d_?\s*o_?)\s*r(\\_?\s*)-?r(\\_?\s*)-?f\s*

4. Unicode Characters and Homoglyphs:

Users might use similar-looking characters from different character sets to bypass simple text matching.

Example:

\s*[sSsS](u|U|u|U)(d|D|d|D)(o|O|o|O)\s*r\s*-{0,2}r(\s*|\s*-){0,2}f\s*

5. Case Insensitivity:

Ensure your regex is case insensitive to capture all uppercase and lowercase variations.

(?i)\b(sudo\s+rm\s+-?r?-?f)\b

6. Word Boundaries:

Using word boundaries (\b) to avoid partial matches.

\b(?:sudo|su|s u d o)\s+rm\s+(?:-r|-r\s+|-r\s+-\s*f|\s+-r\s+\s*f)\b

Example Combined Pattern:

(?i)\b(?:sudo|s u d o)\s+rm\s+(?:-r\s*-f|-rf|-r\s*-?\s*f)\b

7. Testing:

Thoroughly test your regex with various test cases including different contexts, spacing variations, and common legitimate uses of similar phrases.

8. Use a Test Suite:

Create a suite of test messages and see how your regex performs on both blocking jokes and allowing legitimate content.

Example Test Cases:

necoarc0 commented 2 weeks ago

sorry i'm going to research more 😭 @kzndotsh

kzndotsh commented 2 weeks ago

1. Define Your Test Cases

Create a list of test cases that covers all the scenarios you need to consider. Split them into two categories: Positive Cases (which should be blocked) and Negative Cases (which should be allowed).

Positive Cases (should match):

Negative Cases (should not match):

2. Choose a Testing Environment

You can use various platforms and languages to run your tests. Python is a good choice because of its simplicity and powerful regex library.

3. Write the Test Code

Here’s an example of how you could set up and run these tests in Python:

import re

# Your regex pattern
pattern = re.compile(r"(?i)\b(?:sudo|s u d o)\s+rm\s+(?:-r\s*-f|-rf|-r\s*-?\s*f)\b")

# Define test cases
test_cases = {
    "positive": [
        "sudo rm -rf /",
        "S U D O r m -r -f",
        "sudo   rm    -rf",
        "suDo rM - R F",
        "sudo rm -r f"
    ],
    "negative": [
        "sudo rm -r file",
        "Let's discuss sudo and rm commands",
        "The command sudo rm -r file is dangerous",
        "sudorrmf",
        "sudo    rm    myfile"
    ]
}

# Run tests
def run_tests():
    success = True
    print("Running Positive Test Cases\n--------------------------")
    for test_case in test_cases["positive"]:
        if pattern.search(test_case):
            print(f"PASSED: '{test_case}' was correctly flagged.")
        else:
            success = False
            print(f"FAILED: '{test_case}' was not flagged as expected.")

    print("\nRunning Negative Test Cases\n--------------------------")
    for test_case in test_cases["negative"]:
        if pattern.search(test_case):
            success = False
            print(f"FAILED: '{test_case}' was incorrectly flagged.")
        else:
            print(f"PASSED: '{test_case}' was correctly allowed.")

    if success:
        print("\nAll tests passed successfully!")
    else:
        print("\nSome tests failed. Please review the failed cases.")

# Execute tests
run_tests()

4. Automate the Tests

If you want to continually test as you update your regex, consider integrating these tests into a CI/CD pipeline using a tool like GitHub Actions, Travis CI, or Jenkins.

Example using pytest for more automated testing:

You can use the pytest framework to make it more structured and scalable:

import re
import pytest

# Your regex pattern
pattern = re.compile(r"(?i)\b(?:sudo|s u d o)\s+rm\s+(?:-r\s*-f|-rf|-r\s*-?\s*f)\b")

# Define test cases
positive_cases = [
    "sudo rm -rf /",
    "S U D O r m -r -f",
    "sudo   rm    -rf",
    "suDo rM - R F",
    "sudo rm -r f"
]

negative_cases = [
    "sudo rm -r file",
    "Let's discuss sudo and rm commands",
    "The command sudo rm -r file is dangerous",
    "sudorrmf",
    "sudo    rm    myfile"
]

@pytest.mark.parametrize("test_case", positive_cases)
def test_positive_cases(test_case):
    assert pattern.search(test_case), f"Positive test case failed: {test_case}"

@pytest.mark.parametrize("test_case", negative_cases)
def test_negative_cases(test_case):
    assert not pattern.search(test_case), f"Negative test case failed: {test_case}"

if __name__ == "__main__":
    pytest.main()

5. Review and Iterate

After running the tests, review any failed cases, adjust your regex pattern, and rerun the tests until you get the desired results.

kzndotsh commented 2 weeks ago

sorry i'm going to research more 😭 @kzndotsh

im just brain dumping stuff rn no worries

kzndotsh commented 2 weeks ago
necoarc0 commented 2 weeks ago

we need ai to generate more test case @kzndotsh

abxh commented 2 weeks ago

This seems like a herculean task with the range of unicode characters that exist. E.g. of a test case not covered: there exists unicode character to replace (long) spaces (⠀, U+2800).

>>> "rm\u2800-rf\u2800/42"
'rm⠀-rf⠀/42'

I think a pragmatic way of doing it is auto-modding the most seen and manually doing it for the rest if spotted.

arutonee1 commented 2 weeks ago

My first instinct is a (sudo|doas)?\s+rm\s+((-[frR]+|--force|--recursive|--no-preserve-root)\s+)+(/[a-zA-Z\.]*|~|/var/log)(\s|$) with the i flag.

You can of course replace any instance of / with [/...] where ... are variants of /, and similarly for \s. If you really needed to, you could also insert [...]* between every character, where ... are the zero-width unicode characters, but I think that'd be overkill.

I think its reasonable to only catch common cases as @abxh said and manually mod the rest.

kzndotsh commented 2 weeks ago

Now what scenarios could occur where it falsely flags something e.g. someone instructing someone for a support purpose?

abxh commented 2 weeks ago

I think checking for a stricter criteria where spaced apart letters are not checked for could be considered.

The goal of automodding rm -rf is to prevent newbies from typing out the command accidentally (or directly copy-pasting the command). I believe that is important to consider in a problem like this where it's likely not all cases can be covered.

Viewed from the angle of a newbie, the most likely commands that would mislead a newbie would be: 1) Very slight variations in upper-lowercase. (this can be mitigated with .lower() or regexes that account for that) 2) Variations with unicode chars. I have found a python library that could account for this; Unidecode. 3) ...

Even the above could be ignored. And just the literal commands (in ASCII) that poses a risk could be checked for.

If each letter is spaced apart, in my opinion, the command poses less of risk for newbies.

Note also: The more complex the regex, the worse it is for performance and resource usage.

necoarc0 commented 2 weeks ago

I think checking for a stricter criteria where spaced apart letters are not checked for could be considered.

The goal of automodding rm -rf is to prevent newbies from typing out the command accidentally (or directly copy-pasting the command). I believe that is important to consider in a problem like this where it's likely not all cases can be covered.

Viewed from the angle of a newbie, the most likely commands that would mislead a newbie would be:

  1. Very slight variations in upper-lowercase. (this can be mitigated with .lower() or regexes that account for that)
  2. Variations with unicode chars. I have found a python library that could account for this; Unidecode.
  3. ...

Even the above could be ignored. And just the literal commands (in ASCII) that poses a risk could be checked for.

If each letter is spaced apart, in my opinion, the command poses less of risk for newbies.

Note also: The more complex the regex, the worse it is for performance and resource usage.

the regex is to prevent trolls

Jordanyay commented 2 weeks ago

Now what scenarios could occur where it falsely flags something e.g. someone instructing someone for a support purpose?

There's bound to be a scenario where a support person may provide a rm -rf command, so the context of the command will be important to consider.

The most common targets of the rm trolls are usually the root "/" and home "~" directories, these may be utilized as a starting point for filtering out nonmalicious rm commands, but it isn't perfect, for example, a troll could easily bypass said filters by using command substitution:

sudo rm -rf "$(pwd)"

This Bash command invokes "pwd" to print the full filename of the current working directory, which in most cases would be a substitute for "~".

Another case would be the usage of the dot symbol "." to substitute the current directory:

sudo rm -rf .
arutonee1 commented 1 week ago

I genuinely think just filtering out /, common /... dirs, and ~ only is reasonable, and letting everything fall back to manual modding. Any more than that will have too many false positives imo

I do really like @abxh's idea with unidecode or similar libraries. Don't see why .lower() has to be used though, since Python regex has /re/i.

Patterns that I think we should filter out automatically:

/
/bin/?
/boot/?
/dev/?
/etc/?
/home/?
/lib/?
/opt/?
/proc/?
/root/?
/run/?
/sbin/?
/sys/?
/usr/?
/var/?
~/?
/*/?

I can see /home/... being used occasionally in install support, and ., although rare, has popped up for me a few times, so I haven't included those. Any other cases to add, or weird edge cases warranting a removal from this list?