solidx-sysadm 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bash_minus_minus/__init__.py +17 -0
- bash_minus_minus/__main__.py +3 -0
- bash_minus_minus/cli.py +124 -0
- bash_minus_minus/command_forms.py +140 -0
- bash_minus_minus/policy.py +103 -0
- bash_minus_minus/remote_policy.py +430 -0
- bash_minus_minus/runner.py +181 -0
- bash_minus_minus/session.py +97 -0
- bash_minus_minus/suggestions.py +63 -0
- bash_minus_minus/workflows.py +174 -0
- solidx_sysadm-0.1.0b1.dist-info/METADATA +356 -0
- solidx_sysadm-0.1.0b1.dist-info/RECORD +33 -0
- solidx_sysadm-0.1.0b1.dist-info/WHEEL +5 -0
- solidx_sysadm-0.1.0b1.dist-info/entry_points.txt +2 -0
- solidx_sysadm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
- solidx_sysadm-0.1.0b1.dist-info/top_level.txt +2 -0
- sysadm/__init__.py +15 -0
- sysadm/__main__.py +3 -0
- sysadm/agent.py +533 -0
- sysadm/choices.py +45 -0
- sysadm/cli.py +222 -0
- sysadm/config.py +217 -0
- sysadm/events.py +43 -0
- sysadm/extensions.py +222 -0
- sysadm/models.py +175 -0
- sysadm/resources/skills/system-inspection/SKILL.md +26 -0
- sysadm/resources/system_prompt.md +40 -0
- sysadm/security.py +86 -0
- sysadm/storage.py +80 -0
- sysadm/tools.py +236 -0
- sysadm/tui.py +789 -0
- sysadm/ui.py +451 -0
- sysadm/workflows_compat.py +10 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Deterministic inspection policy and execution gateway."""
|
|
2
|
+
|
|
3
|
+
from .policy import Decision, classify as classify_with_policy
|
|
4
|
+
from .remote_policy import PolicyError, load_policy
|
|
5
|
+
from .runner import execute
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def classify(command: str) -> Decision:
|
|
9
|
+
"""Classify against the current managed policy, refreshing it when due.
|
|
10
|
+
|
|
11
|
+
Raises PolicyError if configured central policy cannot be loaded.
|
|
12
|
+
Use policy.classify for pure, explicitly supplied policy classification.
|
|
13
|
+
"""
|
|
14
|
+
return classify_with_policy(command, policy=load_policy().policy)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["Decision", "PolicyError", "classify", "execute", "load_policy"]
|
bash_minus_minus/cli.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .policy import classify
|
|
8
|
+
from .remote_policy import PolicyError, PolicyStore
|
|
9
|
+
from .runner import execute
|
|
10
|
+
from .suggestions import external_instruction
|
|
11
|
+
from .workflows import bacpac_plan, render_plan
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _terminal_safe(text: str) -> str:
|
|
15
|
+
# Retain newlines; escape terminal controls, including ANSI/OSC sequences.
|
|
16
|
+
return "".join(c if c == "\n" or c.isprintable() else ascii(c)[1:-1] for c in text)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def show_plan(env_file=None, output_directory=None, *, as_json=False):
|
|
20
|
+
plan = bacpac_plan(env_file, output_directory)
|
|
21
|
+
print(json.dumps(plan.as_dict()) if as_json else _terminal_safe(render_plan(plan)))
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_command(command, *, as_json=False):
|
|
26
|
+
result = execute(command)
|
|
27
|
+
suggestion = None
|
|
28
|
+
if result.decision.status == "prohibited":
|
|
29
|
+
suggestion = external_instruction(command)
|
|
30
|
+
if result.policy_warning:
|
|
31
|
+
print(f"bash--: {_terminal_safe(result.policy_warning)}", file=sys.stderr)
|
|
32
|
+
if as_json:
|
|
33
|
+
data = asdict(result)
|
|
34
|
+
if suggestion:
|
|
35
|
+
data["external_terminal_instruction"] = suggestion
|
|
36
|
+
data["executed"] = False
|
|
37
|
+
print(json.dumps(data))
|
|
38
|
+
else:
|
|
39
|
+
if result.output:
|
|
40
|
+
print(_terminal_safe(result.output), end="")
|
|
41
|
+
if result.error:
|
|
42
|
+
print(f"bash--: {_terminal_safe(result.error)}", file=sys.stderr)
|
|
43
|
+
if suggestion:
|
|
44
|
+
print(_terminal_safe(suggestion))
|
|
45
|
+
if result.stopped:
|
|
46
|
+
print(f"bash--: stopped: {result.stopped}", file=sys.stderr)
|
|
47
|
+
return result.exit_code if 0 <= result.exit_code <= 255 else 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _main(argv: list[str] | None = None) -> int:
|
|
51
|
+
parser = argparse.ArgumentParser(description="Fail-closed Linux and macOS inspection command gateway.")
|
|
52
|
+
parser.add_argument("command", nargs=argparse.REMAINDER, help="Quoted command or command arguments, e.g. ls -litr")
|
|
53
|
+
actions = parser.add_mutually_exclusive_group()
|
|
54
|
+
actions.add_argument("--check", action="store_true", help="Classify without executing")
|
|
55
|
+
parser.add_argument("--json", action="store_true", help="Return machine-readable JSON")
|
|
56
|
+
actions.add_argument("--list", action="store_true", help="List every approved command form")
|
|
57
|
+
actions.add_argument("--sync-policy", action="store_true", help="Fetch policy immediately")
|
|
58
|
+
actions.add_argument("--policy-status", action="store_true", help="Show effective policy metadata")
|
|
59
|
+
actions.add_argument("--repl", action="store_true", help="Start a policy-gated interactive session")
|
|
60
|
+
actions.add_argument("--plan", choices=["sqlserver-bacpac"], help="Print workflow instructions without executing them")
|
|
61
|
+
parser.add_argument("--env-file", help="Application .env used for non-secret database metadata only")
|
|
62
|
+
parser.add_argument("--output-dir", help="Suggested BACPAC output directory; never created by the planner")
|
|
63
|
+
parser.add_argument("--policy-url", help="Persist an HTTPS manifest URL and fetch its policy")
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
tokens = args.command
|
|
66
|
+
if tokens and tokens[0] == "--":
|
|
67
|
+
tokens = tokens[1:]
|
|
68
|
+
args.command = tokens[0] if len(tokens) == 1 else shlex.join(tokens) if tokens else None
|
|
69
|
+
if args.command is not None and (args.repl or args.plan):
|
|
70
|
+
parser.error("--repl and --plan do not accept a command")
|
|
71
|
+
if args.plan:
|
|
72
|
+
return show_plan(args.env_file, args.output_dir, as_json=args.json)
|
|
73
|
+
if args.repl or (args.command is None and not any((args.list, args.check, args.sync_policy, args.policy_status, args.policy_url))):
|
|
74
|
+
from .session import repl
|
|
75
|
+
return repl(env_file=args.env_file, output_directory=args.output_dir, as_json=args.json)
|
|
76
|
+
if args.command is not None and (args.list or args.sync_policy or args.policy_status):
|
|
77
|
+
parser.error("this action does not accept a command")
|
|
78
|
+
store = PolicyStore()
|
|
79
|
+
if args.policy_url:
|
|
80
|
+
store.configure(args.policy_url)
|
|
81
|
+
active = None
|
|
82
|
+
if args.list or args.check or args.sync_policy or args.policy_status or args.policy_url:
|
|
83
|
+
active = store.load(force=args.sync_policy or bool(args.policy_url))
|
|
84
|
+
if active.warning:
|
|
85
|
+
print(f"bash--: {_terminal_safe(active.warning)}", file=sys.stderr)
|
|
86
|
+
if args.sync_policy or args.policy_status or (args.policy_url and args.command is None and not args.list):
|
|
87
|
+
data = {"policy_version": active.policy.version, "source": active.source,
|
|
88
|
+
"revision": active.revision, "expires_at": active.expires_at,
|
|
89
|
+
"operating_system": active.policy.operating_system,
|
|
90
|
+
"supported_os": active.policy.supported_os,
|
|
91
|
+
"warning": active.warning}
|
|
92
|
+
print(json.dumps(data) if args.json else "\n".join(f"{key}: {value}" for key, value in data.items()))
|
|
93
|
+
return 0
|
|
94
|
+
if args.list:
|
|
95
|
+
forms = [" ".join((name, *options)) for name, variants in active.policy.forms.items()
|
|
96
|
+
if name not in active.policy.prohibited for options in variants]
|
|
97
|
+
print(json.dumps({"policy_version": active.policy.version,
|
|
98
|
+
"operating_system": active.policy.operating_system,
|
|
99
|
+
"supported_os": active.policy.supported_os,
|
|
100
|
+
"forms": forms}) if args.json else "\n".join(forms))
|
|
101
|
+
return 0
|
|
102
|
+
if args.command is None:
|
|
103
|
+
parser.error("provide a command or use --list")
|
|
104
|
+
if args.check:
|
|
105
|
+
decision = classify(args.command, policy=active.policy)
|
|
106
|
+
print(json.dumps(asdict(decision)) if args.json else f"{decision.status}: {decision.reason}")
|
|
107
|
+
return 0 if decision.allowed else 2
|
|
108
|
+
return run_command(args.command, as_json=args.json)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv: list[str] | None = None) -> int:
|
|
112
|
+
try:
|
|
113
|
+
return _main(argv)
|
|
114
|
+
except (PolicyError, OSError, ValueError) as exc:
|
|
115
|
+
error = {"error": str(exc), "exit_code": 3}
|
|
116
|
+
if "--json" in (sys.argv[1:] if argv is None else argv):
|
|
117
|
+
print(json.dumps(error))
|
|
118
|
+
else:
|
|
119
|
+
print(f"bash--: {_terminal_safe(str(exc))}", file=sys.stderr)
|
|
120
|
+
return 3
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Reviewed operand grammars shared by Linux and macOS command profiles."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _path(value):
|
|
8
|
+
# No stdin marker, option operands, expansions, or wildcard paths.
|
|
9
|
+
return (bool(value) and not value.startswith(("-", "+"))
|
|
10
|
+
and not any(c in value for c in "$`\\;|&<>(){}[]*?!~#"))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _flags(value, characters):
|
|
14
|
+
return (value.startswith("-") and 1 < len(value) <= 12
|
|
15
|
+
and all(c in characters for c in value[1:]))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sed_print(value):
|
|
19
|
+
address = r"(?:[1-9][0-9]{0,5}|\$)"
|
|
20
|
+
return bool(re.fullmatch(rf"{address}(?:,{address})?p|/[^/\\]{{1,256}}/p", value))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _sed_substitute(value):
|
|
24
|
+
# Slash delimiter only. No backslashes, extra commands, or e/w flags.
|
|
25
|
+
return bool(re.fullmatch(r"s/[^/\\]{1,256}/[^/\\]{0,256}/g?", value))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
VALIDATORS = {
|
|
29
|
+
"<container>": lambda value: bool(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", value)),
|
|
30
|
+
"<files>": _path,
|
|
31
|
+
"<paths>": _path,
|
|
32
|
+
"<directory>": _path,
|
|
33
|
+
"<count>": lambda value: value.isascii() and value.isdecimal() and 1 <= int(value) <= 100000,
|
|
34
|
+
"<depth>": lambda value: value.isascii() and value.isdecimal() and 0 <= int(value) <= 32,
|
|
35
|
+
"<legacy-count>": lambda value: bool(re.fullmatch(r"-[1-9][0-9]{0,4}", value)),
|
|
36
|
+
"<pattern>": lambda value: 1 <= len(value) <= 256 and not value.startswith("-"),
|
|
37
|
+
"<glob>": lambda value: 1 <= len(value) <= 256,
|
|
38
|
+
"<ls-options>": lambda value: _flags(value, "laAhitrSdF"),
|
|
39
|
+
"<cat-options>": lambda value: _flags(value, "nbsvet"),
|
|
40
|
+
"<grep-options>": lambda value: _flags(value, "ivnHhclwoEFx"),
|
|
41
|
+
"<sed-print>": _sed_print,
|
|
42
|
+
"<sed-substitute>": _sed_substitute,
|
|
43
|
+
}
|
|
44
|
+
REPEATS = {"<files>": 16, "<paths>": 16, "<ls-options>": 8,
|
|
45
|
+
"<cat-options>": 8, "<grep-options>": 8}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def match_arguments(arguments, form):
|
|
49
|
+
"""Return (placeholder, value) bindings, or None when the template fails."""
|
|
50
|
+
def match(index, offset):
|
|
51
|
+
if index == len(form):
|
|
52
|
+
return () if offset == len(arguments) else None
|
|
53
|
+
atom = form[index]
|
|
54
|
+
if atom not in VALIDATORS:
|
|
55
|
+
if offset < len(arguments) and arguments[offset] == atom:
|
|
56
|
+
return match(index + 1, offset + 1)
|
|
57
|
+
return None
|
|
58
|
+
bindings = []
|
|
59
|
+
for count in range(1, min(REPEATS.get(atom, 1), len(arguments) - offset) + 1):
|
|
60
|
+
value = arguments[offset + count - 1]
|
|
61
|
+
if not VALIDATORS[atom](value):
|
|
62
|
+
break
|
|
63
|
+
bindings.append((atom, value))
|
|
64
|
+
remainder = match(index + 1, offset + count)
|
|
65
|
+
if remainder is not None:
|
|
66
|
+
return (*bindings, *remainder)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return match(0, 0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def argument_bindings(arguments, forms):
|
|
73
|
+
for form in forms:
|
|
74
|
+
bindings = match_arguments(arguments, form)
|
|
75
|
+
if bindings is not None:
|
|
76
|
+
return bindings
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def has_unquoted_shell_syntax(command):
|
|
81
|
+
"""Allow metacharacters only as quoted data, subsequently grammar-checked."""
|
|
82
|
+
quote = None
|
|
83
|
+
escaped = False
|
|
84
|
+
for character in command:
|
|
85
|
+
if escaped:
|
|
86
|
+
escaped = False
|
|
87
|
+
elif quote:
|
|
88
|
+
if character == quote:
|
|
89
|
+
quote = None
|
|
90
|
+
elif quote == '"' and character == "\\":
|
|
91
|
+
escaped = True
|
|
92
|
+
elif character in "'\"":
|
|
93
|
+
quote = character
|
|
94
|
+
elif character in "$`\\;|&<>(){}[]*?!~#":
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
COMMON_FORMS = {
|
|
100
|
+
"pwd": ((),),
|
|
101
|
+
"pm2": (("list",),),
|
|
102
|
+
"screen": (("-ls",),),
|
|
103
|
+
"sort": (("-h",),),
|
|
104
|
+
"docker": (("--version",), ("version",), ("ps",), ("ps", "-a"),
|
|
105
|
+
("images",), ("volume", "ls"),
|
|
106
|
+
("inspect", "--format", "{{.State.Status}}", "<container>")),
|
|
107
|
+
"dotnet": (("--version",), ("--list-sdks",), ("--list-runtimes",),
|
|
108
|
+
("tool", "list", "--global")),
|
|
109
|
+
"sqlpackage": (("/Version",),),
|
|
110
|
+
"ls": ((), ("<paths>",), ("<ls-options>",), ("<ls-options>", "<paths>")),
|
|
111
|
+
"cat": (("<files>",), ("<cat-options>", "<files>")),
|
|
112
|
+
"head": (("<files>",), ("-n", "<count>", "<files>"),
|
|
113
|
+
("-c", "<count>", "<files>"), ("<legacy-count>", "<files>")),
|
|
114
|
+
"tail": (("<files>",), ("-n", "<count>", "<files>"),
|
|
115
|
+
("-c", "<count>", "<files>"), ("<legacy-count>", "<files>")),
|
|
116
|
+
"grep": (("<pattern>", "<files>"), ("<grep-options>", "<pattern>", "<files>")),
|
|
117
|
+
"sed": (("-n", "<sed-print>", "<files>"), ("<sed-substitute>", "<files>")),
|
|
118
|
+
}
|
|
119
|
+
COMMON_FORMS["find"] = tuple(
|
|
120
|
+
("<directory>", *depth, *kind, *name, *output)
|
|
121
|
+
for depth in ((), ("-maxdepth", "<depth>"))
|
|
122
|
+
for kind in ((), ("-type", "f"), ("-type", "d"))
|
|
123
|
+
for name in ((), ("-name", "<glob>"), ("-iname", "<glob>"))
|
|
124
|
+
for output in ((), ("-print",))
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def parse_invocation(command):
|
|
128
|
+
"""Parse one literal executable invocation; never evaluate shell syntax."""
|
|
129
|
+
if (not isinstance(command, str) or not command.strip() or len(command) > 4096
|
|
130
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in command)
|
|
131
|
+
or has_unquoted_shell_syntax(command)):
|
|
132
|
+
return None
|
|
133
|
+
try:
|
|
134
|
+
argv = tuple(shlex.split(command, comments=False, posix=True))
|
|
135
|
+
except ValueError:
|
|
136
|
+
return None
|
|
137
|
+
if (not argv or not argv[0] or len(argv) > 64
|
|
138
|
+
or any(not part or len(part) > 1024 for part in argv)):
|
|
139
|
+
return None
|
|
140
|
+
return argv
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""A deliberately small command language, not a Bash parser."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Mapping
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .command_forms import COMMON_FORMS, argument_bindings, has_unquoted_shell_syntax
|
|
10
|
+
from .suggestions import safe_argv
|
|
11
|
+
|
|
12
|
+
POLICY_VERSION = "3"
|
|
13
|
+
MAX_COMMAND_LENGTH = 4096
|
|
14
|
+
|
|
15
|
+
# Each complete argument tuple is reviewed independently. No arbitrary operands.
|
|
16
|
+
FORMS = {
|
|
17
|
+
"uptime": ((), ("-p",), ("-s",)),
|
|
18
|
+
"du": (("-xhd1", "<directory>"), ("-sh", "<directory>")),
|
|
19
|
+
"free": ((), ("-h",), ("-m",), ("-b",)),
|
|
20
|
+
"df": ((), ("-h",), ("-i",), ("-T",), ("-hT",)),
|
|
21
|
+
"ps": ((), ("aux",), ("-ef",)),
|
|
22
|
+
"uname": ((), ("-a",), ("-r",), ("-m",)),
|
|
23
|
+
**COMMON_FORMS,
|
|
24
|
+
}
|
|
25
|
+
MACOS_FORMS = {
|
|
26
|
+
"uptime": ((),),
|
|
27
|
+
"du": (("-h", "-d", "1", "-x", "<directory>"), ("-sh", "<directory>")),
|
|
28
|
+
"df": ((), ("-h",), ("-i",)),
|
|
29
|
+
"uname": ((), ("-a",), ("-r",), ("-m",)),
|
|
30
|
+
**COMMON_FORMS,
|
|
31
|
+
}
|
|
32
|
+
OS_FORMS = {"linux": FORMS, "macos": MACOS_FORMS}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def operating_system() -> str:
|
|
36
|
+
return "macos" if sys.platform == "darwin" else sys.platform
|
|
37
|
+
# Any executable may be requested and explicitly approved. Central manifest
|
|
38
|
+
# denials remain authoritative; there is no client-side executable catalog.
|
|
39
|
+
PROHIBITED = frozenset()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Policy:
|
|
44
|
+
version: str
|
|
45
|
+
forms: Mapping[str, tuple[tuple[str, ...], ...]]
|
|
46
|
+
prohibited: frozenset[str]
|
|
47
|
+
supported_os: tuple[str, ...] = ("linux",)
|
|
48
|
+
operating_system: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def builtin_policy() -> Policy:
|
|
52
|
+
system = operating_system()
|
|
53
|
+
return Policy(POLICY_VERSION, MappingProxyType(OS_FORMS.get(system, {})),
|
|
54
|
+
PROHIBITED, tuple(OS_FORMS), system)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
BUILTIN_POLICY = builtin_policy()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Decision:
|
|
62
|
+
status: str
|
|
63
|
+
code: str
|
|
64
|
+
reason: str
|
|
65
|
+
argv: tuple[str, ...] = ()
|
|
66
|
+
policy_version: str = POLICY_VERSION
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def allowed(self) -> bool:
|
|
70
|
+
return self.status == "allowed"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def classify(command: str, *, policy: Policy = BUILTIN_POLICY) -> Decision:
|
|
74
|
+
"""Pure classification. Never reads the OS, expands input, or runs a command."""
|
|
75
|
+
def decision(*args):
|
|
76
|
+
if len(args) > 3 and args[0] != "allowed":
|
|
77
|
+
args = (*args[:3], safe_argv(args[3]))
|
|
78
|
+
return Decision(*args, policy_version=policy.version)
|
|
79
|
+
|
|
80
|
+
if policy.operating_system is not None and policy.operating_system not in policy.supported_os:
|
|
81
|
+
return decision("unsupported", "unsupported_os", "Policy does not support this operating system.")
|
|
82
|
+
if not isinstance(command, str) or not command.strip():
|
|
83
|
+
return decision("unsupported", "empty_input", "Provide one inspection command.")
|
|
84
|
+
if len(command) > MAX_COMMAND_LENGTH:
|
|
85
|
+
return decision("unsupported", "input_limit", "Command exceeds 4096 characters.")
|
|
86
|
+
# Metacharacters are permitted only as quoted, grammar-checked operands.
|
|
87
|
+
if any(ord(c) < 32 or ord(c) == 127 for c in command):
|
|
88
|
+
return decision("unsupported", "character", "Control characters are not supported.")
|
|
89
|
+
if has_unquoted_shell_syntax(command):
|
|
90
|
+
return decision("unsupported", "shell_syntax", "Shell operators and expansions are unsupported.")
|
|
91
|
+
try:
|
|
92
|
+
argv = tuple(shlex.split(command, comments=False, posix=True))
|
|
93
|
+
except ValueError:
|
|
94
|
+
return decision("unsupported", "quoting", "Input contains incomplete quoting.")
|
|
95
|
+
if not argv or not argv[0]:
|
|
96
|
+
return decision("unsupported", "empty_input", "Provide one inspection command.")
|
|
97
|
+
if argv[0].rsplit("/", 1)[-1] in policy.prohibited:
|
|
98
|
+
return decision("prohibited", "prohibited_command", "This executable is prohibited.", argv)
|
|
99
|
+
if argv[0] not in policy.forms:
|
|
100
|
+
return decision("unsupported", "unknown_command", "Executable has no inspection policy.", argv)
|
|
101
|
+
if argument_bindings(argv[1:], policy.forms[argv[0]]) is None:
|
|
102
|
+
return decision("unsupported", "arguments", "Arguments do not match an approved command form.", argv)
|
|
103
|
+
return decision("allowed", "inspection", "Approved inspection command form.", argv)
|