shellsafe 0.1.0__tar.gz
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.
- shellsafe-0.1.0/.github/workflows/ci.yml +28 -0
- shellsafe-0.1.0/.github/workflows/release.yml +31 -0
- shellsafe-0.1.0/.gitignore +12 -0
- shellsafe-0.1.0/CHANGELOG.md +14 -0
- shellsafe-0.1.0/CONTRIBUTING.md +12 -0
- shellsafe-0.1.0/LICENSE +21 -0
- shellsafe-0.1.0/PKG-INFO +126 -0
- shellsafe-0.1.0/README.md +104 -0
- shellsafe-0.1.0/examples/demo.py +33 -0
- shellsafe-0.1.0/pyproject.toml +71 -0
- shellsafe-0.1.0/src/shellsafe/__init__.py +18 -0
- shellsafe-0.1.0/src/shellsafe/__main__.py +3 -0
- shellsafe-0.1.0/src/shellsafe/_version.py +1 -0
- shellsafe-0.1.0/src/shellsafe/audit/__init__.py +10 -0
- shellsafe-0.1.0/src/shellsafe/audit/rules.py +0 -0
- shellsafe-0.1.0/src/shellsafe/cli.py +44 -0
- shellsafe-0.1.0/src/shellsafe/errors.py +36 -0
- shellsafe-0.1.0/src/shellsafe/execute.py +117 -0
- shellsafe-0.1.0/src/shellsafe/exitcodes.py +6 -0
- shellsafe-0.1.0/src/shellsafe/platforms.py +35 -0
- shellsafe-0.1.0/src/shellsafe/py.typed +0 -0
- shellsafe-0.1.0/src/shellsafe/raw.py +30 -0
- shellsafe-0.1.0/src/shellsafe/render.py +192 -0
- shellsafe-0.1.0/src/shellsafe/reporters.py +1 -0
- shellsafe-0.1.0/tests/golden/.gitkeep +0 -0
- shellsafe-0.1.0/tests/integration/.gitkeep +0 -0
- shellsafe-0.1.0/tests/integration/test_exec_posix.py +44 -0
- shellsafe-0.1.0/tests/payloads/cases/README.md +3 -0
- shellsafe-0.1.0/tests/payloads/cases/case_01_semicolon.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_02_substitution.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_03_backticks.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_04_background_chain.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_05_or_chain.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_06_redirect_out.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_07_redirect_in.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_08_pipe_out.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_09_quote_smuggle.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_10_quote_storm.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_11_globs_expansions.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_12_newline.txt +2 -0
- shellsafe-0.1.0/tests/payloads/cases/case_13_fullwidth_semicolon.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_14_whitespace.txt +1 -0
- shellsafe-0.1.0/tests/payloads/cases/case_15_unicode_dashes.txt +1 -0
- shellsafe-0.1.0/tests/property/.gitkeep +0 -0
- shellsafe-0.1.0/tests/property/test_invariants.py +35 -0
- shellsafe-0.1.0/tests/unit/test_cli.py +11 -0
- shellsafe-0.1.0/tests/unit/test_errors.py +13 -0
- shellsafe-0.1.0/tests/unit/test_payload_corpus.py +43 -0
- shellsafe-0.1.0/tests/unit/test_public_surface.py +19 -0
- shellsafe-0.1.0/tests/unit/test_raw.py +28 -0
- shellsafe-0.1.0/tests/unit/test_render.py +155 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
continue-on-error: ${{ matrix.experimental == true }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
include:
|
|
16
|
+
- { os: ubuntu-latest, python: "3.14" }
|
|
17
|
+
- { os: ubuntu-latest, python: "3.14t", experimental: true }
|
|
18
|
+
- { os: macos-latest, python: "3.14" }
|
|
19
|
+
- { os: windows-latest, python: "3.14" }
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- run: uv sync
|
|
26
|
+
- run: uv run pytest -q
|
|
27
|
+
- run: uv run ruff check .
|
|
28
|
+
- run: uv run mypy src/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- run: uv build
|
|
14
|
+
- run: uv run twine check dist/*
|
|
15
|
+
- uses: actions/upload-artifact@v4
|
|
16
|
+
with:
|
|
17
|
+
name: dist
|
|
18
|
+
path: dist/
|
|
19
|
+
|
|
20
|
+
publish-pypi:
|
|
21
|
+
needs: build
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment: pypi
|
|
24
|
+
permissions:
|
|
25
|
+
id-token: write
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/download-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist
|
|
31
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Format follows
|
|
4
|
+
Keep a Changelog; versioning follows SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-08-24
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Argv-mode rendering and execution via template strings
|
|
11
|
+
- RAW trust marker with argv splicing
|
|
12
|
+
- capture() helper with utf-8 stdout/stderr
|
|
13
|
+
- plan() helper for inspecting commands before execution
|
|
14
|
+
- Injection payload corpus as a release gate; all cases render inert
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Issues and PRs welcome.
|
|
4
|
+
|
|
5
|
+
1. Run the verification commands (pytest, ruff, mypy) after every change; PRs
|
|
6
|
+
must arrive green.
|
|
7
|
+
2. Security-relevant changes require corpus or property coverage in the same PR.
|
|
8
|
+
3. Commit subjects follow Conventional Commits.
|
|
9
|
+
4. Prose follows plain technical writing: no marketing adjectives, state limits.
|
|
10
|
+
|
|
11
|
+
Report security vulnerabilities privately to the maintainer address in
|
|
12
|
+
pyproject.toml; do not open public issues.
|
shellsafe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Sharma (rahulXs)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
shellsafe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shellsafe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe shell commands via Python 3.14 template strings. Injection-proof by construction.
|
|
5
|
+
Project-URL: Repository, https://github.com/rahulXs/shellsafe
|
|
6
|
+
Project-URL: Issues, https://github.com/rahulXs/shellsafe/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/rahulXs/shellsafe/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Rahul Sharma <rahulxsh@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: automation,command-injection,injection,pep750,security,shell,subprocess,t-strings,template-strings
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.14
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# shellsafe
|
|
24
|
+
|
|
25
|
+
> Safe shell commands via Python 3.14 template strings. Injection-proof by
|
|
26
|
+
> construction.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from shellsafe import run
|
|
30
|
+
|
|
31
|
+
message = get_user_input() # "fix; rm -rf ~"
|
|
32
|
+
run(t"git commit -m {message}")
|
|
33
|
+
# argv: ["git", "commit", "-m", "fix; rm -rf ~"]
|
|
34
|
+
# one command; the scary text is just an argument
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
The dominant pattern in scripts and automation is still this:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
subprocess.run(f"git commit -m {message}", shell=True) # injection waiting to happen
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python 3.14 template strings (`t"..."`) separate static text from interpolated
|
|
46
|
+
values. shellsafe turns that structure into argv lists where interpolated values
|
|
47
|
+
are always data, never commands. When you genuinely need pipes, shell mode quotes
|
|
48
|
+
every value with POSIX rules first.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install shellsafe
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.14+ (template strings).
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
Run a command. Interpolated values are always single arguments:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from shellsafe import run
|
|
64
|
+
|
|
65
|
+
run(t"mkdir {path}")
|
|
66
|
+
run(t"docker build -t {tag} .", check=True, timeout=300)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Capture output as text:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from shellsafe import capture
|
|
73
|
+
|
|
74
|
+
res = capture(t"grep {pattern} {file}")
|
|
75
|
+
print(res.stdout, res.returncode)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pipes and redirections on POSIX (values are quoted with `shlex.quote` first):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from shellsafe import shx
|
|
82
|
+
|
|
83
|
+
shx(t"cat {file} | wc -l")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Inspect exactly what will execute:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from shellsafe import plan # lower-level: render without running
|
|
90
|
+
|
|
91
|
+
print(plan(t"git commit -m {message}"))
|
|
92
|
+
# argv: ["git","commit","-m","fix; rm -rf ~"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## What it refuses
|
|
96
|
+
|
|
97
|
+
| Case | Behavior |
|
|
98
|
+
|---|---|
|
|
99
|
+
| Interpolation as the executable | error: the command comes from static text only |
|
|
100
|
+
| Shell route on Windows | error: cmd.exe quoting cannot be made injection-safe; use argv mode |
|
|
101
|
+
| RAW misuse | error: one argument, verbatim, nesting refused |
|
|
102
|
+
|
|
103
|
+
`RAW("...")` / `RAW(["a", "b"])` is the single explicit trust boundary for
|
|
104
|
+
pre-quoted content. Every use site is greppable.
|
|
105
|
+
|
|
106
|
+
## Limits
|
|
107
|
+
|
|
108
|
+
- Windows: interpolated shell routes are refused rather than approximated;
|
|
109
|
+
argv-mode commands work fully.
|
|
110
|
+
- Bytes interpolations are rejected: decode explicitly first.
|
|
111
|
+
- Runtime behavior after import is your test suite's job, same trust model as
|
|
112
|
+
calling subprocess yourself.
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Issues and PRs welcome. Security reports go privately to the maintainer, never
|
|
117
|
+
through public issues.
|
|
118
|
+
|
|
119
|
+
## Requirements
|
|
120
|
+
|
|
121
|
+
- CPython >= 3.14 (uses template strings from PEP 750)
|
|
122
|
+
- Linux, macOS, Windows (Windows supports argv mode; POSIX-only shell mode)
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# shellsafe
|
|
2
|
+
|
|
3
|
+
> Safe shell commands via Python 3.14 template strings. Injection-proof by
|
|
4
|
+
> construction.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from shellsafe import run
|
|
8
|
+
|
|
9
|
+
message = get_user_input() # "fix; rm -rf ~"
|
|
10
|
+
run(t"git commit -m {message}")
|
|
11
|
+
# argv: ["git", "commit", "-m", "fix; rm -rf ~"]
|
|
12
|
+
# one command; the scary text is just an argument
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
The dominant pattern in scripts and automation is still this:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
subprocess.run(f"git commit -m {message}", shell=True) # injection waiting to happen
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.14 template strings (`t"..."`) separate static text from interpolated
|
|
24
|
+
values. shellsafe turns that structure into argv lists where interpolated values
|
|
25
|
+
are always data, never commands. When you genuinely need pipes, shell mode quotes
|
|
26
|
+
every value with POSIX rules first.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install shellsafe
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.14+ (template strings).
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Run a command. Interpolated values are always single arguments:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from shellsafe import run
|
|
42
|
+
|
|
43
|
+
run(t"mkdir {path}")
|
|
44
|
+
run(t"docker build -t {tag} .", check=True, timeout=300)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Capture output as text:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from shellsafe import capture
|
|
51
|
+
|
|
52
|
+
res = capture(t"grep {pattern} {file}")
|
|
53
|
+
print(res.stdout, res.returncode)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pipes and redirections on POSIX (values are quoted with `shlex.quote` first):
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from shellsafe import shx
|
|
60
|
+
|
|
61
|
+
shx(t"cat {file} | wc -l")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Inspect exactly what will execute:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from shellsafe import plan # lower-level: render without running
|
|
68
|
+
|
|
69
|
+
print(plan(t"git commit -m {message}"))
|
|
70
|
+
# argv: ["git","commit","-m","fix; rm -rf ~"]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## What it refuses
|
|
74
|
+
|
|
75
|
+
| Case | Behavior |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Interpolation as the executable | error: the command comes from static text only |
|
|
78
|
+
| Shell route on Windows | error: cmd.exe quoting cannot be made injection-safe; use argv mode |
|
|
79
|
+
| RAW misuse | error: one argument, verbatim, nesting refused |
|
|
80
|
+
|
|
81
|
+
`RAW("...")` / `RAW(["a", "b"])` is the single explicit trust boundary for
|
|
82
|
+
pre-quoted content. Every use site is greppable.
|
|
83
|
+
|
|
84
|
+
## Limits
|
|
85
|
+
|
|
86
|
+
- Windows: interpolated shell routes are refused rather than approximated;
|
|
87
|
+
argv-mode commands work fully.
|
|
88
|
+
- Bytes interpolations are rejected: decode explicitly first.
|
|
89
|
+
- Runtime behavior after import is your test suite's job, same trust model as
|
|
90
|
+
calling subprocess yourself.
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Issues and PRs welcome. Security reports go privately to the maintainer, never
|
|
95
|
+
through public issues.
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- CPython >= 3.14 (uses template strings from PEP 750)
|
|
100
|
+
- Linux, macOS, Windows (Windows supports argv mode; POSIX-only shell mode)
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Side-by-side demo: the classic injection, rendered inert.
|
|
2
|
+
|
|
3
|
+
Run from an environment where shellsafe is importable:
|
|
4
|
+
|
|
5
|
+
python examples/demo.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from string.templatelib import Interpolation, Template
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
|
13
|
+
|
|
14
|
+
from shellsafe.render import plan
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
message = "fix; rm -rf ~"
|
|
19
|
+
|
|
20
|
+
print("UNSAFE (f-string into a shell):")
|
|
21
|
+
print(f" $ git commit -m {message}")
|
|
22
|
+
print(" -> two commands; the second one deletes your home directory")
|
|
23
|
+
print()
|
|
24
|
+
|
|
25
|
+
print("SAFE (shellsafe):")
|
|
26
|
+
p = plan(Template("git commit -m ", Interpolation(message, "message", None, "")))
|
|
27
|
+
print(f" $ {p!r}")
|
|
28
|
+
print(" -> one command; the scary text is just an argument")
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shellsafe"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Safe shell commands via Python 3.14 template strings. Injection-proof by construction."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
authors = [{ name = "Rahul Sharma", email = "rahulxsh@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"shell", "subprocess", "injection", "security", "template-strings",
|
|
15
|
+
"t-strings", "pep750", "command-injection", "automation",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
shellsafe = "shellsafe.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/rahulXs/shellsafe"
|
|
34
|
+
Issues = "https://github.com/rahulXs/shellsafe/issues"
|
|
35
|
+
Changelog = "https://github.com/rahulXs/shellsafe/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "src/shellsafe/_version.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/shellsafe"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
target-version = "py314"
|
|
45
|
+
line-length = 100
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
strict = true
|
|
52
|
+
python_version = "3.14"
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-q --tb=short"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
# RAW() is a deliberate loud trust marker; the uppercase name is the API.
|
|
60
|
+
"src/shellsafe/raw.py" = ["N802"]
|
|
61
|
+
"src/shellsafe/__init__.py" = ["N999"]
|
|
62
|
+
|
|
63
|
+
[dependency-groups]
|
|
64
|
+
dev = [
|
|
65
|
+
"pytest>=8",
|
|
66
|
+
"ruff>=0.5",
|
|
67
|
+
"mypy>=1.10",
|
|
68
|
+
"hypothesis>=6",
|
|
69
|
+
"build>=1.2",
|
|
70
|
+
"twine>=6",
|
|
71
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""shellsafe: safe shell commands via Python 3.14 template strings.
|
|
2
|
+
|
|
3
|
+
Public surface (stable names, additive only through 1.0):
|
|
4
|
+
run, capture, shx, RAW, CaptureResult.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._version import __version__
|
|
8
|
+
from .execute import CaptureResult, capture, run, shx
|
|
9
|
+
from .raw import RAW
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RAW",
|
|
13
|
+
"CaptureResult",
|
|
14
|
+
"__version__",
|
|
15
|
+
"capture",
|
|
16
|
+
"run",
|
|
17
|
+
"shx",
|
|
18
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Offline AST audit for dangerous command construction. Ships with v0.3.0."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors import ShellSafeError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def scan(paths: list[str]) -> list[dict[str, object]]:
|
|
9
|
+
"""Scan paths for AU001-AU004 findings. Arrives in shellsafe 0.3."""
|
|
10
|
+
raise ShellSafeError("audit arrives in shellsafe 0.3")
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CLI wiring: audit, demo, version. Business logic lives in the stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import exitcodes
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="shellsafe")
|
|
15
|
+
parser.add_argument("-V", "--version", action="store_true")
|
|
16
|
+
sub = parser.add_subparsers(dest="command")
|
|
17
|
+
|
|
18
|
+
sub.add_parser("audit", help="scan code for dangerous command construction (v0.3)")
|
|
19
|
+
sub.add_parser("version", help="detailed version and capability matrix")
|
|
20
|
+
return parser
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _print_version_matrix() -> None:
|
|
24
|
+
py = platform.python_version()
|
|
25
|
+
print(f"shellsafe {__version__} · python {py} · {sys.platform}")
|
|
26
|
+
print("argv-mode: available")
|
|
27
|
+
print("shell-mode: arriving in 0.2")
|
|
28
|
+
print("audit: arriving in 0.3")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(argv: list[str] | None = None) -> int:
|
|
32
|
+
args = _build_parser().parse_args(argv)
|
|
33
|
+
|
|
34
|
+
if args.version:
|
|
35
|
+
print(f"shellsafe {__version__}")
|
|
36
|
+
return exitcodes.OK
|
|
37
|
+
if args.command == "version":
|
|
38
|
+
_print_version_matrix()
|
|
39
|
+
return exitcodes.OK
|
|
40
|
+
if args.command == "audit":
|
|
41
|
+
print("audit arrives in shellsafe 0.3", file=sys.stderr)
|
|
42
|
+
return exitcodes.USAGE
|
|
43
|
+
_build_parser().print_help()
|
|
44
|
+
return exitcodes.OK
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Exception hierarchy for shellsafe.
|
|
2
|
+
|
|
3
|
+
Every error raised publicly inherits from ShellSafeError so callers can catch
|
|
4
|
+
broadly. Messages are lowercase, state got and expected, and include the fix.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ShellSafeError(Exception):
|
|
9
|
+
"""Base class for every error shellsafe raises."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShellSafeTypeError(ShellSafeError):
|
|
13
|
+
"""A template or interpolated value has an unsupported type."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnsupportedPlatformError(ShellSafeError):
|
|
17
|
+
"""The requested shell route cannot be made safe on this platform.
|
|
18
|
+
|
|
19
|
+
Interpolated shell routes are refused on Windows by policy; restructure the
|
|
20
|
+
command as argv mode (no pipes or redirections) instead.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArgvOnlyError(ShellSafeError):
|
|
25
|
+
"""shx() was called with a template that needs no shell at all.
|
|
26
|
+
|
|
27
|
+
Use run() for plain commands; shx() exists for pipes and redirections.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RawUsageError(ShellSafeError):
|
|
32
|
+
"""RAW() was used incorrectly.
|
|
33
|
+
|
|
34
|
+
RAW takes exactly one argument: a list of strings in argv mode or a string
|
|
35
|
+
in shell mode. RAW values are never nested and never modified.
|
|
36
|
+
"""
|