Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,38 @@ def _gh_option(words: list[str], flag: str, short: str = "", boolean: bool = Fal
return value.lower()


def _starts_automatically(prog: str, lower: list[str]) -> str | None:
"""Changes that schedule work after this command has finished."""
if prog == "schtasks":
action = next((w for w in lower[1:] if w in
("/create", "/query", "/change", "/delete", "/run", "/end", "/showsid")), None)
if action == "/create":
return "creates a scheduled task that can run after this session (schtasks /create)"
if prog == "register-scheduledtask":
return "registers a task that can run after this session (Register-ScheduledTask)"
if (prog == "reg" and lower[1:2] == ["add"]) or prog in ("new-itemproperty", "set-itemproperty"):
if any(re.search(r"(?:^|\\)currentversion\\run(?:once)?$", w.rstrip("\\")) for w in lower[1:]):
return "adds a Windows Run key value that starts a program at login"
if prog == "systemctl":
args = iter(lower[1:])
for word in args:
if word in ("-h", "--host", "-m", "--machine", "--root", "--image"):
next(args, None)
continue
if word.startswith("-"):
continue
if word == "enable":
return "enables a service to start automatically (systemctl enable)"
break
if prog == "launchctl" and lower[1:2] == ["load"]:
return "loads a launch agent or daemon that can run after this session (launchctl load)"
if prog == "crontab" and "-l" not in lower and "-r" not in lower:
if "-e" in lower or "-" in lower[1:] or _positional(lower):
return "installs or edits scheduled commands that will run later (crontab)"
return None



def classify(words: list[str], raw: str = "", depth: int = 0) -> tuple[list[Command], list[str]]:
"""One simple command (already split into words). -> (commands, hidden-reasons)."""
words = words[1:] if words[:1] == ["{"] else words # a group, `{ curl URL > x.sh; } && bash x.sh`
Expand Down Expand Up @@ -853,6 +885,9 @@ def classify(words: list[str], raw: str = "", depth: int = 0) -> tuple[list[Comm
if prog == "crontab" and "-r" in lower:
cmd.kind, cmd.why = "irreversible", "deletes every scheduled job (crontab -r)"
return [cmd], []
if (why := _starts_automatically(prog, lower)):
cmd.kind, cmd.why = "irreversible", why
return [cmd], []
if prog == "truncate" or prog in ("clear-content", "clc") or (prog == "mv" and any(w in ("/dev/null", "nul") for w in lower[1:])) \
or (prog == "cp" and lower[1:2] == ["/dev/null"] and len(_positional(words)) == 2):
cmd.kind, cmd.why = "irreversible", f"empties or discards files ({' '.join(words[:3])})"
Expand Down
46 changes: 46 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,49 @@ def test_gh_harmless_lookalikes_keep_existing_classification(line):
])
def test_gh_irreversible_reasons_describe_the_operation(line, reason):
assert reason in commands.read(line).summary()


# These are inert command-reader inputs. No task, registry change or service is executed.
@pytest.mark.parametrize("line", [
r"schtasks /create /sc onlogon /tn updater /tr C:\Users\me\x.exe",
r"SCHTASKS.EXE /Create /SC DAILY /TN example /TR C:\example.exe",
"Register-ScheduledTask -TaskName updater -Action $a -Trigger $t",
r"reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v updater /d x.exe",
r'reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v test /d x.exe',
r"New-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Run -Name updater -Value x.exe",
r'Set-ItemProperty -LiteralPath "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name updater -Value x.exe',
"systemctl enable --now example.service",
"sudo systemctl --user enable example.service",
"systemctl --host example.invalid enable example.service",
"launchctl load -w ~/Library/LaunchAgents/com.example.plist",
"crontab -",
"crontab schedule.txt",
"crontab -e",
'(crontab -l; echo "@reboot echo example") | crontab -',
])
def test_startup_changes_wait_for_a_person(line):
reading = commands.read(line)
assert reading.kind == "irreversible", reading.summary()
assert reading.summary()


@pytest.mark.parametrize("line", [
"schtasks /query",
"schtasks /query /tn enable",
"systemctl status nginx",
"systemctl status enable",
"systemctl --user status nginx",
"launchctl list",
"launchctl print system/com.example",
"crontab -l",
"crontab -u root -l",
"Get-ScheduledTask",
r"reg query HKCU\Software\Microsoft\Windows\CurrentVersion\Run",
r"Get-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Run",
r"reg add HKCU\Software\Example\Settings /v mode /d default",
r"New-ItemProperty -Path HKCU:\Software\Example\RunHistory -Name test -Value example",
"echo systemctl enable example.service",
])
def test_startup_queries_and_lookalikes_are_not_persistence_changes(line):
reading = commands.read(line)
assert reading.kind in ("read_only", "other"), reading.summary()
Loading