From 5f4c9fb88d132738011904b5b357aacd97131def Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Mon, 21 Aug 2023 21:43:54 -0400 Subject: [PATCH] Split regex functions and non-regex functions (#171) * Makes CommandContains regex by default * Split regex functions and non-regex functions * Update documentation to reflect what checks have regex support. * Fix only Regex or only Not bug * lowercase regex fix * version bump to 2.1.0 * break to continue --- README.md | 2 +- checks.go | 54 +++++++------- checks_linux.go | 7 ++ configs.go | 6 ++ docs/checks.md | 123 ++++++++++++++++++++++---------- docs/examples/linux-remote.conf | 2 +- docs/regex.md | 40 ++++++++--- utility.go | 2 +- utility_linux.go | 1 + 9 files changed, 163 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 6b13810e..5de1ed5b 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ user = "coolUser" # Main user for the image # if re-used, is compatible with the version of aeacus being used. # # You can print your version of aeacus with ./aeacus version. -version = "2.0.6" +version = "2.1.0" [[check]] message = "Removed insecure sudoers rule" diff --git a/checks.go b/checks.go index 33fc09e5..9f607335 100644 --- a/checks.go +++ b/checks.go @@ -40,6 +40,7 @@ type cond struct { Key string Value string After string + regex bool } // requireArgs is a convenience function that prints a warning if any required @@ -55,7 +56,7 @@ func (c cond) requireArgs(args ...interface{}) { v := reflect.ValueOf(c) vType := v.Type() for i := 0; i < v.NumField(); i++ { - if vType.Field(i).Name == "Type" { + if vType.Field(i).Name == "Type" || vType.Field(i).Name == "regex" { continue } @@ -111,21 +112,24 @@ func runCheck(cond cond) bool { debug("Running condition:\n", cond) not := "Not" + regex := "Regex" condFunc := "" negation := false + cond.regex = false // Ensure that condition type is a valid length - if len(cond.Type) <= len(not) { + if len(cond.Type) <= len(regex) { fail(`Condition type "` + cond.Type + `" is not long enough to be valid. Do you have a "type = 'CheckTypeHere'" for all check conditions?`) return false } - - condEnding := cond.Type[len(cond.Type)-len(not) : len(cond.Type)] - if condEnding == not { + condFunc = cond.Type + if cond.Type[len(cond.Type)-len(not):len(cond.Type)] == not { negation = true condFunc = cond.Type[:len(cond.Type)-len(not)] - } else { - condFunc = cond.Type + } + if cond.Type[len(cond.Type)-len(regex):len(cond.Type)] == regex { + cond.regex = true + condFunc = cond.Type[:len(cond.Type)-len(regex)] } // Catch panic if check type doesn't exist @@ -150,24 +154,30 @@ func runCheck(cond cond) bool { return err.IsNil() && result } -// CommandContains checks if a given shell command contains a certain output. +// CommandContains checks if a given shell command contains a certain string. // This check will always fail if the command returns an error. func (c cond) CommandContains() (bool, error) { c.requireArgs("Cmd", "Value") out, err := shellCommandOutput(c.Cmd) + if err != nil { + return false, err + } + if c.regex { + outTrim := strings.TrimSpace(out) + return regexp.Match(c.Value, []byte(outTrim)) + } return strings.Contains(strings.TrimSpace(out), c.Value), err } -// CommandOutput checks if a given shell command produces an exact output. This -// check will always fail if the command returns an error. +// CommandOutput checks if a given shell command produces an exact output. +// This check will always fail if the command returns an error. func (c cond) CommandOutput() (bool, error) { c.requireArgs("Cmd", "Value") out, err := shellCommandOutput(c.Cmd) return strings.TrimSpace(out) == c.Value, err } -// DirContains returns true if any file in the directory matches the regular -// expression provided. +// DirContains returns true if any file in the directory contains the string value provided. func (c cond) DirContains() (bool, error) { c.requireArgs("Path", "Value") result, err := cond{ @@ -208,11 +218,6 @@ func (c cond) DirContains() (bool, error) { return false, nil } -// DirContainsRegex is an alias for DirContains -func (c cond) DirContainsRegex() (bool, error) { - return c.DirContains() -} - // FileContains determines whether a file contains a given regular expression. // // Newlines in regex may not work as expected, especially on Windows. It's @@ -225,9 +230,13 @@ func (c cond) FileContains() (bool, error) { } found := false for _, line := range strings.Split(fileContent, "\n") { - found, err = regexp.Match(c.Value, []byte(line)) - if err != nil { - return false, err + if c.regex { + found, err = regexp.Match(c.Value, []byte(line)) + if err != nil { + return false, err + } + } else { + found = strings.Contains(line, c.Value) } if found { break @@ -236,11 +245,6 @@ func (c cond) FileContains() (bool, error) { return found, err } -// FileContainsRegex is an alias for FileContains -func (c cond) FileContainsRegex() (bool, error) { - return c.FileContains() -} - // FileEquals calculates the SHA256 sum of a file and compares it with the hash // provided in the check. func (c cond) FileEquals() (bool, error) { diff --git a/checks_linux.go b/checks_linux.go index 3470a7f7..519a0460 100644 --- a/checks_linux.go +++ b/checks_linux.go @@ -13,6 +13,7 @@ func (c cond) AutoCheckUpdatesEnabled() (bool, error) { result, err := cond{ Path: "/etc/apt/apt.conf.d/", Value: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, + regex: true, }.DirContains() // If /etc/apt/ does not exist, try dnf (RHEL) if err != nil { @@ -26,6 +27,7 @@ func (c cond) AutoCheckUpdatesEnabled() (bool, error) { applyUpdates, err := cond{ Path: "/etc/dnf/automatic.conf", Value: `(?i)^\s*apply_updates\s*=\s*(1|on|yes|true)`, + regex: true, }.FileContains() if err != nil { return false, err @@ -80,6 +82,7 @@ func (c cond) FirewallUp() (bool, error) { result, err := cond{ Path: "/etc/ufw/ufw.conf", Value: `^\s*ENABLED=yes\s*$`, + regex: true, }.FileContains() if err != nil { // If ufw.conf does not exist, check firewalld status (RHEL) @@ -95,11 +98,13 @@ func (c cond) GuestDisabledLDM() (bool, error) { result, err := cond{ Path: "/usr/share/lightdm/lightdm.conf.d/", Value: guestStr, + regex: true, }.DirContains() if !result { return cond{ Path: "/etc/lightdm/", Value: guestStr, + regex: true, }.DirContains() } return result, err @@ -245,6 +250,7 @@ func (c cond) UserExists() (bool, error) { return cond{ Path: "/etc/passwd", Value: "^" + c.User + ":", + regex: true, }.FileContains() } @@ -253,5 +259,6 @@ func (c cond) UserInGroup() (bool, error) { return cond{ Path: "/etc/group", Value: c.Group + `[0-9a-zA-Z,:\s+]+` + c.User, + regex: true, }.FileContains() } diff --git a/configs.go b/configs.go index 3d31a040..7c41a931 100644 --- a/configs.go +++ b/configs.go @@ -197,6 +197,9 @@ func obfuscateConfig() { func obfuscateCond(c *cond) error { s := reflect.ValueOf(c).Elem() for i := 0; i < s.NumField(); i++ { + if s.Type().Field(i).Name == "regex" { + continue + } datum := s.Field(i).String() if err := obfuscateData(&datum); err != nil { return err @@ -211,6 +214,9 @@ func obfuscateCond(c *cond) error { func deobfuscateCond(c *cond) error { s := reflect.ValueOf(c).Elem() for i := 0; i < s.NumField(); i++ { + if s.Type().Field(i).Name == "regex" { + continue + } datum := s.Field(i).String() if err := deobfuscateData(&datum); err != nil { return err diff --git a/docs/checks.md b/docs/checks.md index 853173ea..29a27313 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -2,16 +2,22 @@ ## Checks -This is a list of vulnerability checks that can be used in the configuration for `aeacus`. The notes on this page contain a lot of important information, please be sure to read them. +This is a list of vulnerability checks that can be used in the configuration for `aeacus`. The notes on this page +contain a lot of important information, please be sure to read them. -> **Note**: Each of the commands here can check for the opposite by appending 'Not' to the check type. For example, `PathExistsNot` to pass if a file does not exist. +> **Note**: Each of the commands here can check for the opposite by appending 'Not' to the check type. For +> example, `PathExistsNot` to pass if a file does not exist. > **Note**: If a check has negative points assigned to it, it automatically becomes a penalty. -> **Note**: Each of these check types can be used for `Pass`, `PassOverride` or `Fail` conditions, and there can be multiple conditions per check. See [configuration](config.md) for more details. +> **Note**: Each of these check types can be used for `Pass`, `PassOverride` or `Fail` conditions, and there can be +> multiple conditions per check. See [configuration](config.md) for more details. +> **Note**: Regex is officially supported for `CommandContainsRegex`, `DirContainsRegex`, and `FileContainsRegex`. Read +> more about regex [here](regex.md). -**CommandContains**: pass if command output contains string. If executing the command fails (the check returns an error), check never passes. Use of this check is discouraged. +**CommandContains**: pass if command output contains string. If executing the command fails (the check returns an +error), check never passes. Use of this check is discouraged. ``` type = 'CommandContains' @@ -19,10 +25,17 @@ cmd = 'ufw status' value = 'Status: active' ``` -> **Note**: `Command*` checks are prone to interception, modification, and tomfoolery. Your scoring configuration will be much more robust if you rely on checks using native mechanisms rather than shell commands (for example, `PathExists` instead of ls). -> **Note**: If any check returns an error (e.g., something that it was not expecting), it will _never_ pass, even if it's a `Not` condition. This varies by check, but for example, if you try to check the content of a file that doesn't exist, it will return an error and not succeed-- even if you were doing `FileContainsNot`. +> **Note**: `Command*` checks are prone to interception, modification, and tomfoolery. Your scoring configuration will +> be much more robust if you rely on checks using native mechanisms rather than shell commands (for +> example, `PathExists` +> instead of ls). -**CommandOutput**: pass if command output matches string exactly. If it returns an error, check never passes. Use of this check is discouraged. +> **Note**: If any check returns an error (e.g., something that it was not expecting), it will _never_ pass, even if +> it's a `Not` condition. This varies by check, but for example, if you try to check the content of a file that doesn't +> exist, it will return an error and not succeed-- even if you were doing `FileContainsNot`. + +**CommandOutput**: pass if command output matches string exactly. If it returns an error, check never passes. Use of +this check is discouraged. ``` type = 'CommandOutput' @@ -30,9 +43,7 @@ cmd = '(Get-NetFirewallProfile -Name Domain).Enabled' value = 'True' ``` -**DirContains**: pass if directory contains regular expression (regex) string - -> **Note**: Read more about regex [here](regex.md). +**DirContains**: pass if directory contains a string value ``` type = 'DirContains' @@ -40,14 +51,19 @@ path = '/etc/sudoers.d/' value = 'NOPASSWD' ``` -> `DirContains` is recursive! This means it checks every folder and subfolder. It currently is capped at 10,000 files, so you should begin your search at the deepest folder possible. +> `DirContains` is recursive! This means it checks every folder and subfolder. It currently is capped at 10,000 files, +> so you should begin your search at the deepest folder possible. -> **Note**: You don't have to escape any characters because we're using single quotes, which are literal strings in TOML. If you need use single quotes, use a TOML multi-line string literal `''' like this! that's neat! C:\path\here '''`), or just normal quotes (but you'll have to escape characters with those). +> **Note**: You don't have to escape any characters because we're using single quotes, which are literal strings in +> TOML. If you need use single quotes, use a TOML multi-line string +> literal `''' like this! that's neat! C:\path\here '''`), or just normal quotes (but you'll have to escape characters +> with those). -**FileContains**: pass if file contains regex +**FileContains**: pass if file contains a value -> **Note**: `FileContains` will never pass if file does not exist! Add an additional PassOverride check for PathExistsNot, if you want to score that a file does not contain a line, OR it doesn't exist. +> **Note**: `FileContains` will never pass if file does not exist! Add an additional PassOverride check for +> PathExistsNot, if you want to score that a file does not contain a line, OR it doesn't exist. ``` type = 'FileContains' @@ -77,7 +93,8 @@ path = '/etc/passwd' name = 'root' ``` -> Get owner of the file in both Windows and Linux. You can see the owner of a file on Windows using PowerShell: `(Get-Acl [FILENAME]).Owner`. For Linux, use `ls -la FILENAME`. +> Get owner of the file in both Windows and Linux. You can see the owner of a file on Windows using +> PowerShell: `(Get-Acl [FILENAME]).Owner`. For Linux, use `ls -la FILENAME`. **FirewallUp**: pass if firewall is active @@ -86,11 +103,14 @@ name = 'root' type = 'FirewallUp' ``` -> **Note**: On Linux, `ufw` (checks `/etc/ufw/ufw.conf`) and `firewalld` are supported. If the `ufw` config does not exist, the engine checks if `firewalld` is running. On Window, this passes if all three Windows Firewall profiles are active. +> **Note**: On Linux, `ufw` (checks `/etc/ufw/ufw.conf`) and `firewalld` are supported. If the `ufw` config does not +> exist, the engine checks if `firewalld` is running. On Window, this passes if all three Windows Firewall profiles are +> active. **PasswordChanged**: pass if user password has changed -For Linux, check if user's password hash is not next to their username in `/etc/shadow`. If you don't use the whole hash, make sure you start it from the beginning (typically `$X$...` where X is a number). +For Linux, check if user's password hash is not next to their username in `/etc/shadow`. If you don't use the whole +hash, make sure you start it from the beginning (typically `$X$...` where X is a number). ``` type = 'PasswordChanged' @@ -108,7 +128,8 @@ user = 'username' after = 'Monday, January 02, 2006 3:04:05 PM' ``` -> You should take the value from `(Get-LocalUser ).PasswordLastSet` and use it as `after`. This check will never pass if the user does not exist, so don't use this with users that should be removed. +> You should take the value from `(Get-LocalUser ).PasswordLastSet` and use it as `after`. This check will +> never pass if the user does not exist, so don't use this with users that should be removed. **PathExists**: pass if specified path exists. This works for both files AND folders (directories). @@ -124,7 +145,8 @@ path = 'C:\importantfolder\' **PermissionIs**: pass if specified user has specified permission on a given file -For Linux, use the standard octal `rwx` format (`ls -la yourfile` will show them). Use question marks to omit bits you don't care about. +For Linux, use the standard octal `rwx` format (`ls -la yourfile` will show them). Use question marks to omit bits you +don't care about. ``` type = 'PermissionIs' @@ -139,7 +161,10 @@ type = 'PermissionIsNot' path = '/bin/bash' value = '???s????w?' ``` -So if `/bin/bash` is no longer world writable OR no longer SUID, the check will pass. If you want to ensure both attributes are removed, you should use two conditions in the same check (`pass` for writeable bit, in addition to `pass` for SUID bit). + +So if `/bin/bash` is no longer world writable OR no longer SUID, the check will pass. If you want to ensure both +attributes are removed, you should use two conditions in the same check (`pass` for writeable bit, in addition to `pass` +for SUID bit). For Windows, get a users permission of the file using `(Get-Acl [FILENAME]).Access`. @@ -152,7 +177,8 @@ value = 'FullControl' > **Note**: Use absolute paths when possible (rather than relative) for more reliable scoring. -**ProgramInstalled**: pass if program is installed. On Linux, will use `dpkg` (or `rpm` for RHEL-based systems), and on Windows, checks if any installed programs contain your program string. +**ProgramInstalled**: pass if program is installed. On Linux, will use `dpkg` (or `rpm` for RHEL-based systems), and on +Windows, checks if any installed programs contain your program string. ``` type = 'ProgramInstalled' @@ -168,6 +194,7 @@ type = 'ProgramVersion' name = 'Firefox' value = '88.0.1+build1-0ubuntu0.20.04.2' ``` + > Only works for `dpkg` based distributions (such as Debian and Ubuntu). For Windows, get versions from `.\aeacus.exe info programs`. @@ -181,26 +208,32 @@ name = 'Firefox' value = '95.0.1' ``` -> **Note**: We recommend you use the `Not` version of this check to score a program's version being different from its version at the beginning of the image. You can't guarantee that the latest version of the program you're scoring will be the same once your round is released, and it's unlikely that a competitor will intentionally downgrade a package. +> **Note**: We recommend you use the `Not` version of this check to score a program's version being different from its +> version at the beginning of the image. You can't guarantee that the latest version of the program you're scoring will +> be +> the same once your round is released, and it's unlikely that a competitor will intentionally downgrade a package. > For packages, Linux uses `dpkg`, Windows uses the Windows API **ServiceUp**: pass if service is running For Linux, use the `systemd` service name. + ``` type = 'ServiceUp' name = 'sshd' ``` For Windows: check the service 'Properties' to find the real service name + ``` type = 'ServiceUp' name = 'tapisrv' # this is telephony ``` -> For services, Linux uses `systemctl`, Windows uses `Get-Service`. If you are using a different init system on Linux, you can use a `Command` check. +> For services, Linux uses `systemctl`, Windows uses `Get-Service`. If you are using a different init system on Linux, +> you can use a `Command` check. **UserExists**: pass if user exists on system @@ -223,13 +256,15 @@ group = 'Administrators' ### Linux-Specific Checks -**AutoCheckUpdatesEnabled**: pass if the system is configured to automatically check for updates (supports `apt` and `dnf-automatic`) +**AutoCheckUpdatesEnabled**: pass if the system is configured to automatically check for updates (supports `apt` +and `dnf-automatic`) ``` type = 'AutoCheckUpdatesEnabled' ``` -**Command**: pass if command succeeds (command is executed, and has a return code of zero). Use of this check is discouraged. This check will NOT return an error if the command is not found +**Command**: pass if command succeeds (command is executed, and has a return code of zero). Use of this check is +discouraged. This check will NOT return an error if the command is not found ``` type = 'Command' @@ -256,11 +291,13 @@ value = '5.4.0-42-generic' ### Windows-Specific Checks -**BitlockerEnabled**: pass if a drive has been fully encrypted with bitlocker drive encription or is in the process of being encrypted +**BitlockerEnabled**: pass if a drive has been fully encrypted with bitlocker drive encription or is in the process of +being encrypted ``` type = "BitlockerEnabled" ``` + > This check will succeed if the drive is either encrypted or encryption is in progress. **FirewallDefaultBehavior**: pass if the firewall profile's default behavior is set to the specified value @@ -271,6 +308,7 @@ name = 'Domain' value = 'Allow' key = 'Inbound' ``` + > Valid "name" (profile) values are: Domain, Public, Private > > Valid "value" (behavior) values are: Allow, Block @@ -286,9 +324,13 @@ key = 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD value = '0' ``` -> **Note**: This check will never pass if retrieving the key fails (wrong hive, key doesn't exist, etc). If you want to check that a key was deleted, use `RegistryKeyExists`. +> **Note**: This check will never pass if retrieving the key fails (wrong hive, key doesn't exist, etc). If you want to +> check that a key was deleted, use `RegistryKeyExists`. -> **Administrative Templates**: There are 4000+ admin template fields. See [this list of registry keys and descriptions](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit?usp=sharing), then use the `RegistryKey` or `RegistryKeyExists` check. +> **Administrative Templates**: There are 4000+ admin template fields. +> +See [this list of registry keys and descriptions](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit?usp=sharing), +> then use the `RegistryKey` or `RegistryKeyExists` check. **RegistryKeyExists**: pass if key exists @@ -297,7 +339,8 @@ type = 'RegistryKeyExists' key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD' ``` -> **Note**: Notice the single quotes `'` on the above argument! This means it's a _string literal_ in TOML. If you don't do this, you have to make sure to escape your slashes (`\` --> `\\`) +> **Note**: Notice the single quotes `'` on the above argument! This means it's a _string literal_ in TOML. If you don't +> do this, you have to make sure to escape your slashes (`\` --> `\\`) > **Note**: You can use `SOFTWARE` as a shortcut for `HKEY_LOCAL_MACHINE\SOFTWARE`. @@ -316,9 +359,11 @@ key = 'DisableCAD' value = '0' ``` -> Values are checking Registry Keys and `secedit.exe` behind the scenes. This means `0` is `Disabled` and `1` is `Enabled`. [See here for reference](securitypolicy.md). +> Values are checking Registry Keys and `secedit.exe` behind the scenes. This means `0` is `Disabled` and `1` +> is `Enabled`. [See here for reference](securitypolicy.md). -> **Note**: For all integer-based values (such as `MinimumPasswordAge`), you can provide a range of values, as seen below. The lower value must be specified first. +> **Note**: For all integer-based values (such as `MinimumPasswordAge`), you can provide a range of values, as seen +> below. The lower value must be specified first. ``` type = 'SecurityPolicy' @@ -334,7 +379,8 @@ name = "TermService" value = "manual" ``` -> This check is a wrapper around RegistryKey to fetch the proper key for you. Also, Automatic (Delayed) and Automatic are the same value for the key we're checking. +> This check is a wrapper around RegistryKey to fetch the proper key for you. Also, Automatic (Delayed) and Automatic +> are the same value for the key we're checking. **ShareExists**: pass if SMB share exists @@ -343,12 +389,14 @@ type = 'ShareExists' name = 'ADMIN$' ``` -> **Note**: Don't use any single quotes (`'`) in your parameters for Windows options like this. If you need to, use a double-quoted string instead (ex. `"Admin's files"`) +> **Note**: Don't use any single quotes (`'`) in your parameters for Windows options like this. If you need to, use a +> double-quoted string instead (ex. `"Admin's files"`) **UserDetail**: pass if user detail key is equal to value -> **Note**: The valid boolean values for this command (when the field is only True or False) are 'yes', if you want the value to be true, or literally anything else for false (like 'no'). +> **Note**: The valid boolean values for this command (when the field is only True or False) are 'yes', if you want the +> value to be true, or literally anything else for false (like 'no'). ``` type = 'UserDetailNot' @@ -361,6 +409,7 @@ value = 'No' > **Note**: For non-boolean details, you can use modifiers in the value field to specify the comparison. > This is specified in the above property document. + ``` type = 'UserDetail' user = 'Administrator' @@ -368,7 +417,6 @@ key = 'PasswordAge' value = '>90' ``` - **UserRights**: pass if specified user or group has specified privilege ``` @@ -377,7 +425,10 @@ name = 'Administrators' value = 'SeTimeZonePrivilege' ``` -> A list of URA and Constant Names (which are used in the config) [can be found here](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment). On your local machine, check Local Security Policy > User Rights Assignments to see the current assignments. +> A list of URA and Constant Names (which are used in the +> +config) [can be found here](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment). +> On your local machine, check Local Security Policy > User Rights Assignments to see the current assignments. **WindowsFeature**: pass if Windows Feature is enabled diff --git a/docs/examples/linux-remote.conf b/docs/examples/linux-remote.conf index 28b4182f..230fcf09 100644 --- a/docs/examples/linux-remote.conf +++ b/docs/examples/linux-remote.conf @@ -2,7 +2,7 @@ name = "linux-remote" # Name of image title = "A Practice Round" # Title of Round user = "sha" # Main user, used for sending notifications os = "Ubuntu 20.04" # Operating system, used for README -version = "2.0.0" # The version of aeacus you're using +version = "2.1.0" # The version of aeacus you're using # If remote is specified, aeacus will report its score and refuse to score if # the remote server does not accept its messages and Team ID (unless "local" is diff --git a/docs/regex.md b/docs/regex.md index 88908860..842f6ac5 100644 --- a/docs/regex.md +++ b/docs/regex.md @@ -1,16 +1,26 @@ # Regular Expressions -Many of the checks in `aeacus` use regular expression (regex) strings as input. This may seem inconvenient if you want to score something simple, but we think it significantly increases the overall quality of checks. Each regex is applied to each line of the input file, so currently, no multi-line regexes are currently possible. +Many of the checks in `aeacus` use regular expression (regex) strings as input. This may seem inconvenient if you want +to score something simple, but we think it significantly increases the overall quality of checks. Each regex is applied +to each line of the input file, so currently, no multi-line regexes are currently possible. -> We're using the Golang Regular Expression package ([documentation here](https://godocs.io/regexp)). It uses RE2 syntax, which is also generally the same as Perl, Python, and other languages. +The checks that are specifically supported are `CommandContainsRegex`, `DirContainsRegex`, and `FileContainsRegex`. +Please note that you **must** add `Regex` for the check to use regular expressions. You can also still append `Not` to +the end to invert the condition, such as `CommandContainsRegexNot` -If you're unfamiliar, a 'regex' is just a way of describing a pattern of text. Let's say I was trying to score this in `/etc/apt/apt.conf.d/*`: +> We're using the Golang Regular Expression package ([documentation here](https://godocs.io/regexp)). It uses RE2 +> syntax, which is also generally the same as Perl, Python, and other languages. + +If you're unfamiliar, a 'regex' is just a way of describing a pattern of text. Let's say I was trying to score this +in `/etc/apt/apt.conf.d/*`: ``` APT::Periodic::Update-Package-Lists "1"; ``` -The most simple regexes work the same way that normal CTRL-f searches work. It just matches what it is. A valid regex to score this would be `APT::Periodic::Update-Package-Lists "1";`, since none of those characters mean anything special to regex. With normal substring searching (like CTRL-f), that's the most specific we would be able to be. +The most simple regexes work the same way that normal CTRL-f searches work. It just matches what it is. A valid regex to +score this would be `APT::Periodic::Update-Package-Lists "1";`, since none of those characters mean anything special to +regex. With normal substring searching (like CTRL-f), that's the most specific we would be able to be. But, what about this? @@ -18,17 +28,21 @@ But, what about this? APT::Periodic::Update-Package-Lists "1"; ``` -Notice that there are now two spaces before the `"1"`. That's a bummer, because our config is still valid to Ubuntu's software updater, but it's not scored as correct. We need at least one space between those two, so we'll do `APT::Periodic::Update-Package-Lists\s+"1";`, where `\s` means any whitespace, and `+` means 'at least one.' +Notice that there are now two spaces before the `"1"`. That's a bummer, because our config is still valid to Ubuntu's +software updater, but it's not scored as correct. We need at least one space between those two, so we'll +do `APT::Periodic::Update-Package-Lists\s+"1";`, where `\s` means any whitespace, and `+` means 'at least one.' Similarly, what about all of these? ``` APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Update-Package-Lists "1" ; - APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Update-Package-Lists "1"; ``` -Our new regex, to match all of those, would be `\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*`. `*` means 'any amount of the preceding token, including none.' So this will match any amount of whitespace (including no whitespace). Which would work much better. +Our new regex, to match all of those, would be `\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*`. `*` means 'any +amount of the preceding token, including none.' So this will match any amount of whitespace (including no whitespace). +Which would work much better. But, what about this? @@ -36,7 +50,9 @@ But, what about this? # APT::Periodic::Update-Package-Lists "1"; ``` -That ruins everything. It would score as correct even though it's commented out. But, it's an easy fix. With the regex `^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, where we added `^` for 'start of the line' and `$` for 'end of the line', it will only match if there's nothing except whitespace before and after the directive. +That ruins everything. It would score as correct even though it's commented out. But, it's an easy fix. With the +regex `^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, where we added `^` for 'start of the line' and `$` for ' +end of the line', it will only match if there's nothing except whitespace before and after the directive. But, what about this? @@ -44,8 +60,12 @@ But, what about this? APT::PERIODIC::UPDATE-PACKAGE-LISTS "1"; ``` -Believe it or not, the apt configs appear to be case insensitive. So we modify the expression to be case insensitive with `(?i)`: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`. +Believe it or not, the apt configs appear to be case-insensitive. So we modify the expression to be case insensitive +with `(?i)`: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`. As far as I know, this is as correct as we can get it. -Thinking about the edge cases and correct grammar for scoring these directives is very important and makes a big difference in scoring robustness, which is why we use regexes for many checks. It can take a lot of practice to get a working expression, and making mistakes is very common. If you want to test your expression interactively, you can use something like [debuggex](debuggex.com). +Thinking about the edge cases and correct grammar for scoring these directives is very important and makes a big +difference in scoring robustness, which is why we use regexes for many checks. It can take a lot of practice to get a +working expression, and making mistakes is very common. If you want to test your expression interactively, you can use +something like [debuggex](https://debuggex.com) or [regex101](https://regex101.com). diff --git a/utility.go b/utility.go index a2d44a9c..d6355b5f 100644 --- a/utility.go +++ b/utility.go @@ -10,7 +10,7 @@ import ( ) const ( - version = "2.0.6" + version = "2.1.0" ) var ( diff --git a/utility_linux.go b/utility_linux.go index 4c5c1811..a3ed8680 100644 --- a/utility_linux.go +++ b/utility_linux.go @@ -46,6 +46,7 @@ func checkTrace() { result, err := cond{ Path: "/proc/self/status", Value: `^TracerPid:\s+0$`, + regex: true, }.FileContains() // If there was an error reading the file, the user may be restricting access to /proc for the phocus binary