bash-classify 0.1.0__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_classify/__init__.py +14 -0
- bash_classify/__main__.py +3 -0
- bash_classify/classifier.py +244 -0
- bash_classify/cli.py +110 -0
- bash_classify/commands/apt.yaml +23 -0
- bash_classify/commands/awk.yaml +5 -0
- bash_classify/commands/basename.yaml +5 -0
- bash_classify/commands/bash.yaml +18 -0
- bash_classify/commands/brew.yaml +25 -0
- bash_classify/commands/builtin.yaml +6 -0
- bash_classify/commands/cargo.yaml +29 -0
- bash_classify/commands/cat.yaml +5 -0
- bash_classify/commands/chgrp.yaml +8 -0
- bash_classify/commands/chmod.yaml +8 -0
- bash_classify/commands/chown.yaml +8 -0
- bash_classify/commands/column.yaml +5 -0
- bash_classify/commands/comm.yaml +5 -0
- bash_classify/commands/command.yaml +10 -0
- bash_classify/commands/cp.yaml +5 -0
- bash_classify/commands/curl.yaml +28 -0
- bash_classify/commands/cut.yaml +5 -0
- bash_classify/commands/date.yaml +8 -0
- bash_classify/commands/df.yaml +5 -0
- bash_classify/commands/diff.yaml +5 -0
- bash_classify/commands/dig.yaml +5 -0
- bash_classify/commands/dirname.yaml +5 -0
- bash_classify/commands/docker.yaml +152 -0
- bash_classify/commands/du.yaml +5 -0
- bash_classify/commands/echo.yaml +5 -0
- bash_classify/commands/env.yaml +14 -0
- bash_classify/commands/expand.yaml +5 -0
- bash_classify/commands/false.yaml +5 -0
- bash_classify/commands/file.yaml +5 -0
- bash_classify/commands/find.yaml +27 -0
- bash_classify/commands/fmt.yaml +5 -0
- bash_classify/commands/fold.yaml +5 -0
- bash_classify/commands/free.yaml +5 -0
- bash_classify/commands/gawk.yaml +5 -0
- bash_classify/commands/git.yaml +441 -0
- bash_classify/commands/go.yaml +38 -0
- bash_classify/commands/grep.yaml +5 -0
- bash_classify/commands/groups.yaml +5 -0
- bash_classify/commands/gunzip.yaml +5 -0
- bash_classify/commands/gzip.yaml +5 -0
- bash_classify/commands/head.yaml +5 -0
- bash_classify/commands/helm.yaml +127 -0
- bash_classify/commands/hostname.yaml +5 -0
- bash_classify/commands/id.yaml +5 -0
- bash_classify/commands/install.yaml +5 -0
- bash_classify/commands/join.yaml +5 -0
- bash_classify/commands/journalctl.yaml +12 -0
- bash_classify/commands/jq.yaml +5 -0
- bash_classify/commands/kill.yaml +5 -0
- bash_classify/commands/killall.yaml +5 -0
- bash_classify/commands/kubectl.yaml +190 -0
- bash_classify/commands/less.yaml +5 -0
- bash_classify/commands/ln.yaml +5 -0
- bash_classify/commands/ls.yaml +5 -0
- bash_classify/commands/make.yaml +19 -0
- bash_classify/commands/md5sum.yaml +5 -0
- bash_classify/commands/mkdir.yaml +5 -0
- bash_classify/commands/more.yaml +5 -0
- bash_classify/commands/mv.yaml +5 -0
- bash_classify/commands/netstat.yaml +5 -0
- bash_classify/commands/nice.yaml +9 -0
- bash_classify/commands/nl.yaml +5 -0
- bash_classify/commands/node.yaml +5 -0
- bash_classify/commands/nohup.yaml +6 -0
- bash_classify/commands/npm.yaml +65 -0
- bash_classify/commands/nslookup.yaml +5 -0
- bash_classify/commands/paste.yaml +5 -0
- bash_classify/commands/ping.yaml +5 -0
- bash_classify/commands/pip.yaml +29 -0
- bash_classify/commands/pip3.yaml +29 -0
- bash_classify/commands/pkill.yaml +5 -0
- bash_classify/commands/poetry.yaml +74 -0
- bash_classify/commands/printenv.yaml +5 -0
- bash_classify/commands/printf.yaml +5 -0
- bash_classify/commands/ps.yaml +5 -0
- bash_classify/commands/pwd.yaml +5 -0
- bash_classify/commands/python.yaml +5 -0
- bash_classify/commands/python3.yaml +5 -0
- bash_classify/commands/read.yaml +5 -0
- bash_classify/commands/readlink.yaml +5 -0
- bash_classify/commands/realpath.yaml +5 -0
- bash_classify/commands/rev.yaml +5 -0
- bash_classify/commands/rm.yaml +5 -0
- bash_classify/commands/rsync.yaml +10 -0
- bash_classify/commands/scp.yaml +5 -0
- bash_classify/commands/sed.yaml +12 -0
- bash_classify/commands/seq.yaml +5 -0
- bash_classify/commands/sh.yaml +12 -0
- bash_classify/commands/sha1sum.yaml +5 -0
- bash_classify/commands/sha256sum.yaml +5 -0
- bash_classify/commands/sort.yaml +8 -0
- bash_classify/commands/ss.yaml +5 -0
- bash_classify/commands/ssh.yaml +39 -0
- bash_classify/commands/stat.yaml +5 -0
- bash_classify/commands/strace.yaml +18 -0
- bash_classify/commands/sudo.yaml +22 -0
- bash_classify/commands/systemctl.yaml +56 -0
- bash_classify/commands/tac.yaml +5 -0
- bash_classify/commands/tail.yaml +5 -0
- bash_classify/commands/tar.yaml +22 -0
- bash_classify/commands/tee.yaml +5 -0
- bash_classify/commands/terraform.yaml +68 -0
- bash_classify/commands/test.yaml +5 -0
- bash_classify/commands/time.yaml +6 -0
- bash_classify/commands/timeout.yaml +15 -0
- bash_classify/commands/top.yaml +5 -0
- bash_classify/commands/touch.yaml +5 -0
- bash_classify/commands/tr.yaml +5 -0
- bash_classify/commands/traceroute.yaml +5 -0
- bash_classify/commands/true.yaml +5 -0
- bash_classify/commands/type.yaml +5 -0
- bash_classify/commands/uname.yaml +5 -0
- bash_classify/commands/unexpand.yaml +5 -0
- bash_classify/commands/uniq.yaml +5 -0
- bash_classify/commands/unzip.yaml +7 -0
- bash_classify/commands/uptime.yaml +5 -0
- bash_classify/commands/uv.yaml +117 -0
- bash_classify/commands/wc.yaml +5 -0
- bash_classify/commands/wget.yaml +5 -0
- bash_classify/commands/whereis.yaml +5 -0
- bash_classify/commands/which.yaml +5 -0
- bash_classify/commands/whoami.yaml +5 -0
- bash_classify/commands/xargs.yaml +25 -0
- bash_classify/commands/xmllint.yaml +5 -0
- bash_classify/commands/yes.yaml +5 -0
- bash_classify/commands/yq.yaml +8 -0
- bash_classify/commands/zip.yaml +5 -0
- bash_classify/commands/zsh.yaml +12 -0
- bash_classify/database.py +257 -0
- bash_classify/matcher.py +702 -0
- bash_classify/models.py +148 -0
- bash_classify/parser.py +525 -0
- bash_classify/py.typed +0 -0
- bash_classify-0.1.0.dist-info/METADATA +147 -0
- bash_classify-0.1.0.dist-info/RECORD +142 -0
- bash_classify-0.1.0.dist-info/WHEEL +4 -0
- bash_classify-0.1.0.dist-info/entry_points.txt +2 -0
- bash_classify-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""bash-classify: Classify bash expressions by their side-effect risk level."""
|
|
2
|
+
|
|
3
|
+
from bash_classify.classifier import classify_expression
|
|
4
|
+
from bash_classify.database import CommandDatabase, load_database
|
|
5
|
+
from bash_classify.models import Classification, CommandResult, ExpressionResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"classify_expression",
|
|
9
|
+
"load_database",
|
|
10
|
+
"CommandDatabase",
|
|
11
|
+
"Classification",
|
|
12
|
+
"ExpressionResult",
|
|
13
|
+
"CommandResult",
|
|
14
|
+
]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Top-level classifier orchestrating parsing, matching, and classification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from .database import load_database
|
|
9
|
+
from .matcher import match_command
|
|
10
|
+
from .models import (
|
|
11
|
+
Classification,
|
|
12
|
+
CommandDef,
|
|
13
|
+
CommandResult,
|
|
14
|
+
ExpressionResult,
|
|
15
|
+
InnerCommandResult,
|
|
16
|
+
Redirect,
|
|
17
|
+
)
|
|
18
|
+
from .parser import parse_expression
|
|
19
|
+
|
|
20
|
+
_SYSTEM_DIRS = (
|
|
21
|
+
"/etc",
|
|
22
|
+
"/lib",
|
|
23
|
+
"/lib64",
|
|
24
|
+
"/usr",
|
|
25
|
+
"/bin",
|
|
26
|
+
"/sbin",
|
|
27
|
+
"/boot",
|
|
28
|
+
"/sys",
|
|
29
|
+
"/proc",
|
|
30
|
+
"/run",
|
|
31
|
+
"/srv",
|
|
32
|
+
"/root",
|
|
33
|
+
"/opt",
|
|
34
|
+
"/var",
|
|
35
|
+
"/dev",
|
|
36
|
+
)
|
|
37
|
+
_SAFE_PREFIXES = (
|
|
38
|
+
"/tmp",
|
|
39
|
+
"/var/tmp",
|
|
40
|
+
"/home",
|
|
41
|
+
"/dev/null",
|
|
42
|
+
"/dev/stdin",
|
|
43
|
+
"/dev/stdout",
|
|
44
|
+
"/dev/stderr",
|
|
45
|
+
"/dev/fd",
|
|
46
|
+
"/dev/tcp",
|
|
47
|
+
"/dev/udp",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_system_path(path: str) -> bool:
|
|
52
|
+
"""Check if a path is a system directory that should trigger DANGEROUS when written to."""
|
|
53
|
+
if not path.startswith("/"):
|
|
54
|
+
return False
|
|
55
|
+
# Check safe prefixes first (more specific)
|
|
56
|
+
for safe in _SAFE_PREFIXES:
|
|
57
|
+
if path == safe or path.startswith(safe + "/"):
|
|
58
|
+
return False
|
|
59
|
+
# Check system dirs
|
|
60
|
+
return any(path == sysdir or path.startswith(sysdir + "/") for sysdir in _SYSTEM_DIRS)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def classify_expression(
|
|
64
|
+
expression: str,
|
|
65
|
+
database: Mapping[str, CommandDef] | None = None,
|
|
66
|
+
) -> ExpressionResult:
|
|
67
|
+
"""Classify a bash expression.
|
|
68
|
+
|
|
69
|
+
Parses the expression, matches each command against the database,
|
|
70
|
+
and returns a composite classification result.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
expression: A bash expression string.
|
|
74
|
+
database: Optional pre-loaded command database. If None, loads the default.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
An ExpressionResult with the overall classification and per-command details.
|
|
78
|
+
"""
|
|
79
|
+
# Step 1: Load database if not provided
|
|
80
|
+
if database is None:
|
|
81
|
+
database = load_database()
|
|
82
|
+
|
|
83
|
+
# Step 2: Parse the expression
|
|
84
|
+
invocations, parse_warnings = parse_expression(expression)
|
|
85
|
+
|
|
86
|
+
# Step 3: Match each command invocation
|
|
87
|
+
command_results: list[CommandResult] = []
|
|
88
|
+
all_redirects: list[Redirect] = []
|
|
89
|
+
|
|
90
|
+
for invocation in invocations:
|
|
91
|
+
# Check for variable expansion in command position
|
|
92
|
+
if invocation.argv and invocation.argv[0].startswith("$"):
|
|
93
|
+
result = CommandResult(
|
|
94
|
+
command=[invocation.argv[0]],
|
|
95
|
+
argv=list(invocation.argv),
|
|
96
|
+
classification=Classification.DANGEROUS,
|
|
97
|
+
matched_rule=None,
|
|
98
|
+
inner_commands=[],
|
|
99
|
+
classification_reason="variable expansion in command position",
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
result = match_command(invocation, database)
|
|
103
|
+
|
|
104
|
+
# Step 4: Apply redirect classification
|
|
105
|
+
for redirect in invocation.redirects:
|
|
106
|
+
if redirect.affects_classification:
|
|
107
|
+
elevated = Classification.max_severity(result.classification, Classification.WRITE)
|
|
108
|
+
if elevated != result.classification:
|
|
109
|
+
result.classification = elevated
|
|
110
|
+
result.classification_reason = (
|
|
111
|
+
f"{result.classification_reason}; elevated by output redirect"
|
|
112
|
+
if result.classification_reason
|
|
113
|
+
else "elevated by output redirect"
|
|
114
|
+
)
|
|
115
|
+
# /dev/tcp and /dev/udp redirects are network access -> DANGEROUS
|
|
116
|
+
if redirect.target.startswith("/dev/tcp/") or redirect.target.startswith("/dev/udp/"):
|
|
117
|
+
elevated = Classification.max_severity(result.classification, Classification.DANGEROUS)
|
|
118
|
+
if elevated != result.classification:
|
|
119
|
+
result.classification = elevated
|
|
120
|
+
result.classification_reason = "elevated to DANGEROUS: /dev/tcp or /dev/udp access detected"
|
|
121
|
+
|
|
122
|
+
# Step 4b: Check for /dev/tcp and /dev/udp in command arguments
|
|
123
|
+
for arg in invocation.argv:
|
|
124
|
+
if arg.startswith("/dev/tcp/") or arg.startswith("/dev/udp/"):
|
|
125
|
+
elevated = Classification.max_severity(result.classification, Classification.DANGEROUS)
|
|
126
|
+
if elevated != result.classification:
|
|
127
|
+
result.classification = elevated
|
|
128
|
+
result.classification_reason = "elevated to DANGEROUS: /dev/tcp or /dev/udp access detected"
|
|
129
|
+
|
|
130
|
+
# Step 5: Apply backgrounding
|
|
131
|
+
if invocation.is_background:
|
|
132
|
+
elevated = Classification.max_severity(result.classification, Classification.WRITE)
|
|
133
|
+
if elevated != result.classification:
|
|
134
|
+
result.classification = elevated
|
|
135
|
+
result.classification_reason = (
|
|
136
|
+
f"{result.classification_reason}; elevated by backgrounding"
|
|
137
|
+
if result.classification_reason
|
|
138
|
+
else "elevated by backgrounding"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Step 6: Elevate to DANGEROUS when writing to system directories
|
|
142
|
+
if result.classification.severity() >= Classification.WRITE.severity():
|
|
143
|
+
system_paths_found = []
|
|
144
|
+
# Check argv tokens
|
|
145
|
+
for token in invocation.argv:
|
|
146
|
+
if _is_system_path(token):
|
|
147
|
+
system_paths_found.append(token)
|
|
148
|
+
# Check redirect targets
|
|
149
|
+
for redirect in invocation.redirects:
|
|
150
|
+
if _is_system_path(redirect.target):
|
|
151
|
+
system_paths_found.append(redirect.target)
|
|
152
|
+
|
|
153
|
+
if system_paths_found and result.classification != Classification.DANGEROUS:
|
|
154
|
+
result.classification = Classification.DANGEROUS
|
|
155
|
+
result.classification_reason = (
|
|
156
|
+
result.classification_reason or ""
|
|
157
|
+
) + f"; elevated to DANGEROUS: system path {system_paths_found[0]}"
|
|
158
|
+
|
|
159
|
+
command_results.append(result)
|
|
160
|
+
all_redirects.extend(invocation.redirects)
|
|
161
|
+
|
|
162
|
+
# Step 7: Collect directories
|
|
163
|
+
directories = _collect_directories(command_results)
|
|
164
|
+
|
|
165
|
+
# Step 8: Compute composite classification
|
|
166
|
+
if not command_results and parse_warnings:
|
|
167
|
+
overall = Classification.UNKNOWN
|
|
168
|
+
elif command_results:
|
|
169
|
+
overall = Classification.max_severity(*(r.classification for r in command_results))
|
|
170
|
+
else:
|
|
171
|
+
overall = Classification.READONLY # empty input
|
|
172
|
+
|
|
173
|
+
return ExpressionResult(
|
|
174
|
+
expression=expression,
|
|
175
|
+
classification=overall,
|
|
176
|
+
directories=directories,
|
|
177
|
+
commands=command_results,
|
|
178
|
+
redirects=all_redirects,
|
|
179
|
+
parse_warnings=parse_warnings,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_directories_from_argv(command: list[str], argv: list[str]) -> list[str]:
|
|
184
|
+
"""Extract directories from a command's argv based on well-known command patterns.
|
|
185
|
+
|
|
186
|
+
Handles find, ls (first positional arg is a directory) and
|
|
187
|
+
cat/head/tail/less/more (dirname of first positional arg containing /).
|
|
188
|
+
"""
|
|
189
|
+
if not command or len(argv) <= 1:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
binary = command[0]
|
|
193
|
+
|
|
194
|
+
# find and ls: first positional (non-option) arg is typically the directory
|
|
195
|
+
if binary in ("find", "ls"):
|
|
196
|
+
for arg in argv[1:]:
|
|
197
|
+
if not arg.startswith("-"):
|
|
198
|
+
return [arg]
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
# File-reading commands: extract dirname from first positional arg containing /
|
|
202
|
+
if binary in ("cat", "head", "tail", "less", "more"):
|
|
203
|
+
for arg in argv[1:]:
|
|
204
|
+
if not arg.startswith("-") and "/" in arg:
|
|
205
|
+
dirname = os.path.dirname(arg)
|
|
206
|
+
if dirname:
|
|
207
|
+
return [dirname]
|
|
208
|
+
return []
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _collect_directories(results: list[CommandResult]) -> list[str]:
|
|
215
|
+
"""Collect directories from command results, including inner commands."""
|
|
216
|
+
directories: list[str] = []
|
|
217
|
+
|
|
218
|
+
for result in results:
|
|
219
|
+
# Directory builtins: cd, pushd, popd
|
|
220
|
+
if result.command and result.command[0] in ("cd", "pushd") and len(result.argv) > 1:
|
|
221
|
+
directories.append(result.argv[1])
|
|
222
|
+
|
|
223
|
+
# Directories captured from global options (e.g., git -C /path)
|
|
224
|
+
if result.directories:
|
|
225
|
+
directories.extend(result.directories)
|
|
226
|
+
|
|
227
|
+
# Well-known commands that take directory arguments
|
|
228
|
+
directories.extend(_extract_directories_from_argv(result.command, result.argv))
|
|
229
|
+
|
|
230
|
+
# Collect directories from inner commands recursively
|
|
231
|
+
directories.extend(_collect_inner_directories(result.inner_commands))
|
|
232
|
+
|
|
233
|
+
return directories
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _collect_inner_directories(results: list[InnerCommandResult]) -> list[str]:
|
|
237
|
+
"""Collect directories from inner command results recursively."""
|
|
238
|
+
directories: list[str] = []
|
|
239
|
+
|
|
240
|
+
for result in results:
|
|
241
|
+
directories.extend(_extract_directories_from_argv(result.command, result.argv))
|
|
242
|
+
directories.extend(_collect_inner_directories(result.inner_commands))
|
|
243
|
+
|
|
244
|
+
return directories
|
bash_classify/cli.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""CLI entry point for bash-classify."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .classifier import classify_expression
|
|
9
|
+
from .models import (
|
|
10
|
+
CommandResult,
|
|
11
|
+
ExpressionResult,
|
|
12
|
+
InnerCommandResult,
|
|
13
|
+
Redirect,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _inner_command_to_dict(result: InnerCommandResult) -> dict:
|
|
18
|
+
"""Convert an InnerCommandResult to a JSON-serializable dict."""
|
|
19
|
+
d: dict = {
|
|
20
|
+
"delegation_mode": result.delegation_mode,
|
|
21
|
+
"delegation_source": result.delegation_source,
|
|
22
|
+
"command": result.command,
|
|
23
|
+
"argv": result.argv,
|
|
24
|
+
"classification": result.classification.value,
|
|
25
|
+
"matched_rule": result.matched_rule,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if result.ignored_options:
|
|
29
|
+
d["ignored_options"] = result.ignored_options
|
|
30
|
+
if result.remaining_options:
|
|
31
|
+
d["remaining_options"] = result.remaining_options
|
|
32
|
+
if result.overriding_option is not None:
|
|
33
|
+
d["overriding_option"] = result.overriding_option
|
|
34
|
+
|
|
35
|
+
d["inner_commands"] = [_inner_command_to_dict(ic) for ic in result.inner_commands]
|
|
36
|
+
|
|
37
|
+
return d
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _command_to_dict(result: CommandResult) -> dict:
|
|
41
|
+
"""Convert a CommandResult to a JSON-serializable dict."""
|
|
42
|
+
d: dict = {
|
|
43
|
+
"command": result.command,
|
|
44
|
+
"argv": result.argv,
|
|
45
|
+
"classification": result.classification.value,
|
|
46
|
+
"matched_rule": result.matched_rule,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if result.ignored_options:
|
|
50
|
+
d["ignored_options"] = result.ignored_options
|
|
51
|
+
if result.remaining_options:
|
|
52
|
+
d["remaining_options"] = result.remaining_options
|
|
53
|
+
if result.classification_reason is not None:
|
|
54
|
+
d["classification_reason"] = result.classification_reason
|
|
55
|
+
if result.overriding_option is not None:
|
|
56
|
+
d["overriding_option"] = result.overriding_option
|
|
57
|
+
|
|
58
|
+
d["inner_commands"] = [_inner_command_to_dict(ic) for ic in result.inner_commands]
|
|
59
|
+
|
|
60
|
+
return d
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _redirect_to_dict(redirect: Redirect) -> dict:
|
|
64
|
+
"""Convert a Redirect to a JSON-serializable dict."""
|
|
65
|
+
return {
|
|
66
|
+
"operator": redirect.operator,
|
|
67
|
+
"target": redirect.target,
|
|
68
|
+
"affects_classification": redirect.affects_classification,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _result_to_dict(result: ExpressionResult) -> dict:
|
|
73
|
+
"""Convert an ExpressionResult to a JSON-serializable dict."""
|
|
74
|
+
d: dict = {
|
|
75
|
+
"expression": result.expression,
|
|
76
|
+
"classification": result.classification.value,
|
|
77
|
+
"directories": result.directories,
|
|
78
|
+
"commands": [_command_to_dict(cmd) for cmd in result.commands],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if result.redirects:
|
|
82
|
+
d["redirects"] = [_redirect_to_dict(r) for r in result.redirects]
|
|
83
|
+
|
|
84
|
+
if result.parse_warnings:
|
|
85
|
+
d["parse_warnings"] = result.parse_warnings
|
|
86
|
+
|
|
87
|
+
return d
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
"""Read bash expressions from stdin, classify, and output JSON.
|
|
92
|
+
|
|
93
|
+
Exit codes:
|
|
94
|
+
0 - successfully classified
|
|
95
|
+
1 - parse error (invalid bash syntax)
|
|
96
|
+
2 - internal error
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
expression = sys.stdin.read().strip()
|
|
100
|
+
if not expression:
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
result = classify_expression(expression)
|
|
104
|
+
output = _result_to_dict(result)
|
|
105
|
+
json.dump(output, sys.stdout, indent=2)
|
|
106
|
+
sys.stdout.write("\n")
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"bash-classify: internal error: {e}", file=sys.stderr)
|
|
110
|
+
sys.exit(2)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: apt
|
|
3
|
+
description: "Debian package management tool"
|
|
4
|
+
strict: false
|
|
5
|
+
subcommands:
|
|
6
|
+
list:
|
|
7
|
+
classification: READONLY
|
|
8
|
+
show:
|
|
9
|
+
classification: READONLY
|
|
10
|
+
search:
|
|
11
|
+
classification: READONLY
|
|
12
|
+
install:
|
|
13
|
+
classification: DANGEROUS
|
|
14
|
+
remove:
|
|
15
|
+
classification: DANGEROUS
|
|
16
|
+
purge:
|
|
17
|
+
classification: DANGEROUS
|
|
18
|
+
update:
|
|
19
|
+
classification: WRITE
|
|
20
|
+
upgrade:
|
|
21
|
+
classification: DANGEROUS
|
|
22
|
+
autoremove:
|
|
23
|
+
classification: DANGEROUS
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: bash
|
|
3
|
+
description: "GNU Bourne-Again SHell"
|
|
4
|
+
classification: DANGEROUS
|
|
5
|
+
strict: false
|
|
6
|
+
delegates_to:
|
|
7
|
+
mode: flag_value_is_expression
|
|
8
|
+
flag: -c
|
|
9
|
+
options:
|
|
10
|
+
-c: {takes_value: true}
|
|
11
|
+
-e: {}
|
|
12
|
+
-x: {}
|
|
13
|
+
-i: {}
|
|
14
|
+
--norc: {}
|
|
15
|
+
--noprofile: {}
|
|
16
|
+
-l: {}
|
|
17
|
+
--login: {aliases: [-l]}
|
|
18
|
+
-n: {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: brew
|
|
3
|
+
description: "macOS and Linux package manager"
|
|
4
|
+
strict: false
|
|
5
|
+
subcommands:
|
|
6
|
+
list:
|
|
7
|
+
classification: READONLY
|
|
8
|
+
info:
|
|
9
|
+
classification: READONLY
|
|
10
|
+
search:
|
|
11
|
+
classification: READONLY
|
|
12
|
+
install:
|
|
13
|
+
classification: WRITE
|
|
14
|
+
uninstall:
|
|
15
|
+
classification: WRITE
|
|
16
|
+
update:
|
|
17
|
+
classification: WRITE
|
|
18
|
+
upgrade:
|
|
19
|
+
classification: WRITE
|
|
20
|
+
cleanup:
|
|
21
|
+
classification: WRITE
|
|
22
|
+
doctor:
|
|
23
|
+
classification: READONLY
|
|
24
|
+
config:
|
|
25
|
+
classification: READONLY
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: cargo
|
|
3
|
+
description: "Rust package manager and build tool"
|
|
4
|
+
strict: false
|
|
5
|
+
subcommands:
|
|
6
|
+
build:
|
|
7
|
+
classification: WRITE
|
|
8
|
+
test:
|
|
9
|
+
classification: WRITE
|
|
10
|
+
run:
|
|
11
|
+
classification: DANGEROUS
|
|
12
|
+
check:
|
|
13
|
+
classification: READONLY
|
|
14
|
+
clippy:
|
|
15
|
+
classification: READONLY
|
|
16
|
+
options:
|
|
17
|
+
--fix: {overrides: WRITE}
|
|
18
|
+
fmt:
|
|
19
|
+
classification: WRITE
|
|
20
|
+
doc:
|
|
21
|
+
classification: WRITE
|
|
22
|
+
publish:
|
|
23
|
+
classification: DANGEROUS
|
|
24
|
+
install:
|
|
25
|
+
classification: WRITE
|
|
26
|
+
clean:
|
|
27
|
+
classification: WRITE
|
|
28
|
+
update:
|
|
29
|
+
classification: WRITE
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: curl
|
|
3
|
+
description: "Transfer data from or to a server"
|
|
4
|
+
classification: WRITE
|
|
5
|
+
strict: false
|
|
6
|
+
options:
|
|
7
|
+
-o: {takes_value: true, overrides: WRITE}
|
|
8
|
+
--output: {takes_value: true, overrides: WRITE, aliases: [-o]}
|
|
9
|
+
-O: {overrides: WRITE}
|
|
10
|
+
--remote-name: {overrides: WRITE, aliases: [-O]}
|
|
11
|
+
-T: {takes_value: true, overrides: WRITE}
|
|
12
|
+
--upload-file: {takes_value: true, overrides: WRITE, aliases: [-T]}
|
|
13
|
+
-X: {takes_value: true}
|
|
14
|
+
--request: {takes_value: true, aliases: [-X]}
|
|
15
|
+
-d: {takes_value: true, overrides: WRITE}
|
|
16
|
+
--data: {takes_value: true, overrides: WRITE, aliases: [-d]}
|
|
17
|
+
-F: {takes_value: true, overrides: WRITE}
|
|
18
|
+
--form: {takes_value: true, overrides: WRITE, aliases: [-F]}
|
|
19
|
+
-H: {takes_value: true}
|
|
20
|
+
--header: {takes_value: true, aliases: [-H]}
|
|
21
|
+
-s: {}
|
|
22
|
+
--silent: {aliases: [-s]}
|
|
23
|
+
-f: {}
|
|
24
|
+
--fail: {aliases: [-f]}
|
|
25
|
+
-L: {}
|
|
26
|
+
--location: {aliases: [-L]}
|
|
27
|
+
-k: {}
|
|
28
|
+
--insecure: {aliases: [-k]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: date
|
|
3
|
+
description: "Display or set the system date and time"
|
|
4
|
+
classification: READONLY
|
|
5
|
+
strict: true
|
|
6
|
+
options:
|
|
7
|
+
-s: {takes_value: true, overrides: DANGEROUS}
|
|
8
|
+
--set: {takes_value: true, overrides: DANGEROUS, aliases: [-s]}
|