Command rule patterns
For: tenant admins and governance reviewers.
Patterns
A pattern names what a command runs, never its text. It is the program (its file name, after its wrappers: sudo rm -rf / is the part rm -rf /), then its flags and operands as the program reads them:
- flags are read as the program does:
-rfis-rand-f,--forceand-fofgit pushare one flag, a shortened long option is the one it shortens, and-target=module.test.*is the flag-targetwith that value; *is one operand,**the rest of the command, and a word with*or?in it is a glob over one operand (module.test.*);[class]is an operand of a class:[home],[root],[inside-workdir],[outside-workdir],[unresolvable],[device],[default-branch], or a URL by its host,[host:get.k3s.io]([host:*.example.com]);a | bnames a partbthat readsa's output: a pipeline's earlier command, a substitution in its arguments or its input.
A program is named by its file name in lower case, with a Windows .exe, .cmd or .bat taken off: on macOS's and Windows' file systems RM and rm.exe run rm, so the reader and every rule read them as rm. A redirection is read as one, never as the program, including {name}>file, which opens a descriptor and assigns its number to name, so {fd}>/dev/null rm -rf ~ is read as rm -rf ~.
Allow matches
An Allow lets through what it writes and no more: the same flags (more only with **), the same number of operands, every value of a flag matching (-target=module.test.* does not allow a second -target elsewhere), and every part that feeds it named. So an Allow of one program never matches a part another program feeds: bash ** lets your scripts run, and still asks about curl ... | bash and bash <(curl ...); to allow a download, name it (curl -fsSL https://get.k3s.io | sh).
An Allow names a program and its arguments. It does not cover an environment variable supplied before it, through env or sudo, or through a client terminal's environment (FOO=1 git log, env FOO=1 git log).
It also excludes variables exported earlier in the command: export FOO=1, declare -x, set -a, set -k, or an assignment to an upper-case name such as PATH=... or a proxy setting. The shell's environment may already hold that name and pass it on. The exceptions only change how output is printed: LANG, LC_*, TZ, TERM, NO_COLOR, FORCE_COLOR, CLICOLOR, COLUMNS, LINES and CI.
An Allow does not cover a file the output goes to. This includes git log > notes.txt, a surrounding { ...; }, loop or if redirected that way, and an earlier exec > notes.txt that redirects every later command. /dev/null and another descriptor (2>&1) are allowed.
Such a command is decided by your other rules and the defaults as if no Allow matched.
Ask and Refuse matches
An Ask or a Refuse catches every part that holds what it writes, whatever else the part adds: rm -r [home] catches rm -rf ~ and sudo rm -r ./x ~.
Patterns that the shell expands
A pattern in the command (*, ? or [...] unquoted, where the reader reads one) is the names the shell makes of it, so a rule matches it by them rather than by its text. An Ask or a Refuse that names a file matches a pattern that may expand to it: a Refuse of cat secrets/prod.key catches cat secrets/prod.ke? and cat secrets/prod.[k]ey, a Refuse of rm a b catches rm *, and a Refuse of grep --include=*.key ** catches grep -r --include=*.ke? x ..
An Allow covers a pattern only where it covers every name the pattern may make: by a class (rm [inside-workdir] ** allows rm build/*.o) in the last operand it lists with a ** after it, since a pattern may be many operands, or for a flag's value by a literal start and a * (--include=src* allows --include=src*.go).
A * or a ? of an Allow stands for one word and one character, so rm x? and rm * do not allow rm x*.
A pattern quoted in the command (cat 'prod.ke?') is one name, and is matched as one.
An Ask or a Refuse matches a pattern in any case, since an account's startup file may set the shell to match names without regard to case: a Refuse of cat notes/todo.txt catches cat NOTES/todo.tx? too.
A class does the same: a path whose first folder is a pattern, or another case, that may make /dev is a [device] where the rest may name a disk, as well as what else it may name.
Variables in matching
A rule matches the words a program gets, a variable's value in them: with a variable the command set itself, d=old; rm -r $d is rm -r old to a rule of it, and s=status; git $s is the git status a rule names, for the defaults as for your rules.
A word whose text the reader cannot know (the command is then unreadable, and the floor asks) may be anything an Ask or a Refuse names, and is nothing an Allow names.
Paths in matching
A word of an Ask or a Refuse that spells a path is compared as a path, and an operand or a flag's value that spells one is read as the path it names, from where the command stands, through the same walk the classes go through.
A leading ./, a doubled /, a ., a .. through a folder, a cd into its folder, a fixed alias and an absolute path all name the same file as the rule's word does, a home folder's path is a path from ~, and a word the rule names as written stays named after a cd.
An absolute path is compared with the working directory when the call says which that is, part by part, so a pattern before it may be it: with /srv/proj as the working directory, cat /srv/p*/secrets/x is a path cat secrets/x names, and so is cat ~/proj/secrets/x where it stands in a home, as a word that climbs reaches the folder above.
A path that never climbed is read as inside the working directory, so it is compared with an absolute or a home word only when the call names that directory: with none, a Refuse of cat /etc/shadow does not name cat shadow.
Where the place a path stands in is not known (the call names no working directory, a cd to a variable, a path that climbed out of it), the two are compared as far as that place leaves open.
A word's wildcard may span folders, so the rest of the path is compared with it, never a count of parts: an absolute path may end in a word from the working directory, and a path that climbed may be how an absolute or a home word ends, or stand one or two parts above a home's: ../../home/kevin/.ssh/x and ../../root/.ssh/x may be ~/.ssh/x, and Refuses of cat secrets/** and cat /etc/* name cat ../p/secrets/a/b and cat ../../etc/ssl/x.
A path that climbed with nothing after it may be any folder such a word names, so a Refuse of rm -rf / names rm -rf ../...
A word with no parts of its own, . or .., is that folder: it names a path of no parts that climbed as far (./, a/.., /proc/self/cwd), and an absolute path only where the call says so.
A URL is no path. An
Allow stays with the words it writes, since a spelling that goes through a folder may leave it by a link.
Uncertain matches
A Refuse never turns into an Ask because the reader could not compare a word: what may match is refused.
That holds for a word whose text it cannot know, in an operand or a flag's value, whether the Refuse writes a word or a class there: rm -rf $X and cat /dev/$X may be what rm -rf [home] and cat [device] name, and a rule over a default narrowed to a class takes what that default found in such a word, as a host or a branch does unless the text the reader does know settles it (https://ok.example/$X is ok.example, and feature/$X is no default branch).
The same rule holds when an unknown word may match a Refuse in any of these ways:
- It may be a flag the Refuse names.
- It may expand to several words, each of which may be an operand the rule lists.
- The pattern starts with the home folder.
**may stand for any depth of folders.- A pattern under
/devhas literal ends that agree with a disk's name, whether or not the reader knows such a disk. - Reading the word passes the reader's work bound. Comparing rules with commands counts toward that bound too, so a very long command under many rules for its program may be refused by the first rule it reaches.
What a Refuse cannot see is a part the reader never reached: a command it could not parse, or the program of one named by a variable, has no such word to compare, and the floor asks about it unless a Refuse over cmd.unreadable makes the floor refuse.
A wrapper never matches by itself: a rule for sudo *, env ..., xargs ... or sh -c * is refused when you save it, since the command it runs is read and decided on its own. So is a rule for eval. A pattern is at most 512 characters and 4 programs, and each of its words at most 256 bytes, since every word is matched on every command.
Command rules
Decide the shell commands a harness runs, part by part: the platform's default rules ask about catastrophic commands on every node, and the tenant's own rules Allow, Ask or Refuse by the command's parse, never its text.
What the command reader understands
What the command reader understands for tenant admins and governance reviewers.