Skip to content

Commit 415fb36

Browse files
authored
Merge pull request #278 from ghantoos/f/engine-v2
Migrates lshell to the canonical engine/config pipeline and updates Python support policy.
2 parents 648f952 + b5154bf commit 415fb36

74 files changed

Lines changed: 3735 additions & 1444 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lshell-tests.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ jobs:
1414
pytest:
1515
name: Pytest Unit/Integration Tests
1616
runs-on: ubuntu-latest
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
python-version: ["3.10", "3.14"]
1721

1822
steps:
1923
- uses: actions/checkout@v4
20-
- name: Set up Python 3.10
24+
- name: Set up Python ${{ matrix.python-version }}
2125
uses: actions/setup-python@v5
2226
with:
23-
python-version: "3.10"
27+
python-version: ${{ matrix.python-version }}
2428
- name: Set up Python path
2529
run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
2630
- name: Install dependencies
@@ -38,8 +42,9 @@ jobs:
3842
name: Lint + Flake8
3943
runs-on: ubuntu-latest
4044
strategy:
45+
fail-fast: false
4146
matrix:
42-
python-version: ["3.8", "3.9", "3.10"]
47+
python-version: ["3.10", "3.14"]
4348

4449
steps:
4550
- uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
Contact: [ghantoos@ghantoos.org](mailto:ghantoos@ghantoos.org)
44
[https://github.com/ghantoos/lshell](https://github.com/ghantoos/lshell)
55

6+
### v0.12.0 (UNRELEASED)
7+
- Packaging/CI: Raised minimum supported Python version to 3.10 (`requires-python >=3.10`), removed EOL Python versions from CI, and aligned docs/package metadata with the new baseline; CI/classifiers now track active CPython release branches 3.10-3.14 (Python 3.6 reached EOL on 23/12/2021).
8+
- Security: Removed regex-driven shell parsing from the authorization flow.
9+
- Engine: Migrated security parsing to a deterministic scanner.
10+
- Refactor: Removed legacy `lshell.parser` compatibility wrapper; runtime and diagnostics now rely on canonical `lshell.engine.*` parsing paths only.
11+
- Refactor: Reorganized configuration code into `lshell/config/` with focused modules (`runtime.py`, `diagnostics.py`, `resolve.py`, `schema.py`) and updated imports accordingly.
12+
- Config: Unified runtime (`CheckConfig`) and diagnostics (`policy-show`) merge logic into a shared resolver to keep section precedence, include overlays, +/- list operations, `all` expansion, and glob-path handling aligned.
13+
- CLI: Restored split diagnostics naming: `policy-show` as the external subcommand and `lshow` as the in-shell builtin.
14+
- CLI: Removed legacy/extra diagnostics commands `lpath`, `lsudo`, `policy-path`, and `policy-sudo`.
15+
- UX: Extended `policy-show` output to include path allow/deny details and sudo policy details directly.
16+
- Tests: Added runtime-vs-diagnostics parity coverage for precedence, include overlays, `allowed=all` minus operations, and schema error behavior.
17+
618
### v0.11.1 21/03/2026
719
- Feature: Added `lshell setup-system` to provision logging paths/permissions and user/group wiring for deployments.
820
- Feature: Added `lshell harden-init` with hardened templates (`sftp-only`, `rsync-backup`, `deploy-minimal`, `readonly-support`) plus `--dry-run`, scoped `[grp:*]`/`[user:*]`, and validation checks.
@@ -19,7 +31,7 @@ Contact: [ghantoos@ghantoos.org](mailto:ghantoos@ghantoos.org)
1931

2032
### v0.11.0 10/03/2026
2133
- Reworked command parsing with a new `pyparsing`-based parser for more reliable command handling.
22-
- Added policy diagnostics and built-ins: `policy-show`, `policy-path`, and `policy-sudo`.
34+
- Added policy diagnostics commands: `policy-show` (CLI) and `lshow` (in-shell).
2335
- Added customizable user-facing messages via the `messages` configuration section.
2436
- Added session `umask` configuration support from `lshell.conf` and CLI overrides.
2537
- Improved `sudo` behavior and command execution handling.
@@ -143,8 +155,8 @@ Contact: [ghantoos@ghantoos.org](mailto:ghantoos@ghantoos.org)
143155

144156
### v0.9.14 27/10/2010
145157
- Corrected `get_aliases` function, as it was looping when aliases were "recursive" (e.g. `ls:ls --color=auto`)
146-
- Added `lsudo` built-in command to list allowed sudo commands.
147-
- Corrected completion function when 2 strings collided (e.g. `ls` and `lsudo`)
158+
- Added diagnostics sudo-policy visibility for allowed sudo commands.
159+
- Corrected completion function when 2 strings collided (for example `ls` with diagnostics commands)
148160
- Corrected the README's installation part (adding `--prefix`).
149161
- Added possibility to log via syslog.
150162
- Corrected warning counter (was counting minus 1).
@@ -179,7 +191,7 @@ Contact: [ghantoos@ghantoos.org](mailto:ghantoos@ghantoos.org)
179191
- Added the possibility to configure introduction prompt.
180192
- Replaced "joker" by "warnings" (more elegant)
181193
- Possibility of limiting the history file size.
182-
- Added `lpath` built-in command to list allowed and denied path. Thanks to Adrien Urban.
194+
- Added diagnostics path-policy visibility for allowed and denied paths. Thanks to Adrien Urban.
183195
- Corrected bug when using `~` was not parsed as "home directory" when used in a command other than `cd`. Thank you Adrien Urban for finding this.
184196
- Corrected minor typo when warning for a forbidden path.
185197
- If `$(foo)` is present in the line, check if `foo` is allowed before executing the line. Thank you Adrien Urban for pointing this out!

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ PyPI project page: https://pypi.org/project/limited-shell/
1010

1111
## Installation
1212

13+
Supported Python versions: `3.10`, `3.11`, `3.12`, `3.13`, `3.14` (minimum `3.10`).
14+
1315
Install from PyPI:
1416

1517
```bash
@@ -95,9 +97,10 @@ lshell policy-show \
9597

9698
Inside an interactive session:
9799

98-
- `policy-show [<command...>]`
99-
- `policy-path` (`lpath` alias)
100-
- `policy-sudo` (`lsudo` alias)
100+
- `lshow [<command...>]`
101+
102+
`lshow` includes effective command policy, allowed/denied paths, and sudo
103+
policy in one output.
101104

102105
Hide these built-ins if needed:
103106

@@ -296,6 +299,7 @@ just test-fuzz-security-parser 20000
296299
Optional local run (if you want to fuzz outside Docker):
297300

298301
```bash
302+
python3 --version # should be >= 3.10
299303
pip install -r requirements-fuzz.txt
300304
python3 fuzz/fuzz_parser_policy.py -runs=20000
301305
```

debian/control

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ Source: lshell
22
Section: shells
33
Priority: optional
44
Maintainer: Ignace Mouzannar <mouzannar@gmail.com>
5-
Build-Depends: debhelper-compat (= 13), python3 (>= 3.4), pybuild-plugin-pyproject
6-
X-Python3-Version: >= 3.4
5+
Build-Depends: debhelper-compat (= 13), python3 (>= 3.10), pybuild-plugin-pyproject
6+
X-Python3-Version: >= 3.10
77
Standards-Version: 3.9.7
88
Homepage: https://github.com/ghantoos/lshell
99

debian/lshell.deb-test.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ allowed_file_extensions : ['.conf', '.log', '.txt']
1313
aliases : {'ll': 'ls -l', 'la': 'ls -la'}
1414
env_vars : {'LSHELL_LAYER': 'default', 'LSHELL_ENV': 'deb-test'}
1515
prompt_short : 1
16-
intro : "\033[1;95mDEB Test Profile: layered default/group/user policy\033[0m\n\n\033[1;92mAllowed examples\033[0m\n\033[90m---------------\033[0m\nid\nwhoami\ndate\nll\ncat /home/testuser/lshell/test/testfiles/test.conf\npolicy-show\npolicy-show cat /home/testuser/lshell/test/testfiles/test.conf\n\n\033[1;91mForbidden examples\033[0m\n\033[90m------------------\033[0m\necho hello\ncat /etc/passwd\ncat /tmp/test.log\n"
16+
intro : "\033[1;95mDEB Test Profile: layered default/group/user policy\033[0m\n\n\033[1;92mAllowed examples\033[0m\n\033[90m---------------\033[0m\nid\nwhoami\ndate\nll\ncat /home/testuser/lshell/test/testfiles/test.conf\nlshow\nlshow cat /home/testuser/lshell/test/testfiles/test.conf\n\n\033[1;91mForbidden examples\033[0m\n\033[90m------------------\033[0m\necho hello\ncat /etc/passwd\ncat /tmp/test.log\n"
1717
messages : {
1818
'unknown_syntax': 'deb-test: unknown syntax -> {command}',
1919
'forbidden_command': 'deb-test: command blocked -> "{command}"',
@@ -35,7 +35,7 @@ env_vars : {'LSHELL_LAYER': 'group'}
3535

3636
[testuser]
3737
# User layer applies on top of default + group for testuser.
38-
allowed : + ['cat', 'head', 'tail', 'policy-show'] - ['echo']
38+
allowed : + ['cat', 'head', 'tail', 'lshow'] - ['echo']
3939
path : + ['/home/testuser/lshell/test/testfiles'] - ['/home']
4040
overssh : + ['ls'] - ['rsync']
4141
allowed_file_extensions : + ['.yaml'] - ['.txt']

docs/engine-migration.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Canonical Engine Architecture
2+
3+
## Overview
4+
5+
Lshell now uses a single canonical command engine for both runtime execution
6+
and policy diagnostics. There is no legacy engine toggle or fallback path.
7+
8+
## Pipeline
9+
10+
The shared flow is:
11+
12+
1. `parse(line)` -> `lshell.engine.ast.ParsedAST`
13+
2. `normalize(parsed_ast)` -> `lshell.engine.ast.CanonicalAST`
14+
3. `authorize(canonical_ast, policy)` -> structured allow/deny decision
15+
4. `execute(decisions, runtime)` -> retcode + audit outcome
16+
17+
Runtime entrypoint:
18+
19+
- `lshell.utils.cmd_parse_execute` -> `lshell.engine.executor.execute_for_shell`
20+
21+
Policy diagnostics entrypoint:
22+
23+
- `lshell.config.diagnostics.policy_command_decision` -> canonical authorizer path
24+
25+
## Engine Modules
26+
27+
- `lshell/engine/ast.py`: canonical AST structures.
28+
- `lshell/engine/parser.py`: top-level command/operator sequence parser.
29+
- `lshell/engine/normalizer.py`: canonical command normalization and assignment splitting.
30+
- `lshell/engine/authorizer.py`: centralized command/path/security authorization.
31+
- `lshell/engine/reasons.py`: structured reason codes and user/audit mappings.
32+
- `lshell/engine/executor.py`: runtime execution with strict-mode and audit semantics.
33+
34+
## Reason Codes
35+
36+
Primary reason codes are defined in `lshell.engine.reasons`, including:
37+
38+
- `allowed`
39+
- `unknown_syntax`
40+
- `forbidden_control_char`
41+
- `forbidden_character`
42+
- `forbidden_path`
43+
- `forbidden_command`
44+
- `forbidden_sudo_command`
45+
- `forbidden_file_extension`
46+
- `forbidden_env_assignment`
47+
- `forbidden_trusted_protocol`
48+
- `command_not_found`
49+
50+
Mappings:
51+
52+
- `to_policy_message(reason)` for policy-show text.
53+
- `to_audit_reason(reason)` for runtime audit logging.
54+
- `warning_payload(reason)` for strict/warning-counter compatibility hooks.
55+
56+
## Validation
57+
58+
- Unit tests: parser/normalizer/authorizer behavior.
59+
- Security regressions: parser smuggling, substitution checks, and path ACL edge cases.

etc/lshell.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,6 @@ strict : 0
223223
#disable_exit : 0
224224

225225
## show/hide policy introspection builtins:
226-
## policy-show, policy-path, policy-sudo (+ aliases lpath, lsudo)
226+
## lshow
227227
## set to 0 to hide these commands from users
228228
#policy_commands : 0

fuzz/fuzz_parser_policy.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
) from exc
1313

1414
with atheris.instrument_imports():
15-
from lshell import parser as lshell_parser
16-
from lshell import policy
15+
from lshell.engine import normalizer as engine_normalizer
16+
from lshell.engine import parser as engine_parser
17+
from lshell.config import diagnostics as policy
1718
from lshell import sec
1819
from lshell import utils
1920

@@ -39,8 +40,6 @@ def info(self, _message):
3940

4041

4142
_FUZZ_TMP = tempfile.mkdtemp(prefix="lshell-fuzz-")
42-
_FUZZ_PARSER = lshell_parser.LshellParser()
43-
4443

4544
def _base_conf():
4645
"""Build an isolated, permissive config for parser/policy fuzz entrypoints."""
@@ -80,9 +79,8 @@ def _fuzz_one_line(line):
8079
"path": conf["path"],
8180
}
8281
try:
83-
parsed = _FUZZ_PARSER.parse(line)
84-
if parsed is not None:
85-
_FUZZ_PARSER.validate_command(parsed)
82+
parsed = engine_parser.parse(line)
83+
engine_normalizer.normalize(parsed)
8684

8785
utils.split_command_sequence(line)
8886
utils.split_commands(line)

lshell/builtincmd.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,21 @@
33
import glob
44
import sys
55
import os
6-
import re
76
import shlex
87
import readline
98
import signal
109

1110
# import lshell specifics
1211
from lshell import variables
1312
from lshell import utils
13+
from lshell import sec as sec_policy
1414

1515

1616
# Store background jobs
1717
BACKGROUND_JOBS = []
1818

1919
POLICY_COMMANDS = [
20-
"policy-show",
21-
"policy-path",
22-
"policy-sudo",
23-
"lpath",
24-
"lsudo",
20+
"lshow",
2521
]
2622

2723
builtins_list = [
@@ -31,11 +27,7 @@
3127
"exit",
3228
"export",
3329
"history",
34-
"policy-show",
35-
"policy-path",
36-
"policy-sudo",
37-
"lpath",
38-
"lsudo",
30+
"lshow",
3931
"help",
4032
"fg",
4133
"bg",
@@ -54,12 +46,15 @@ def _cancel_job_timeout(job):
5446
def cmd_lpath(conf):
5547
"""Show path policy in a concise, readable format."""
5648
current_dir = os.path.realpath(os.getcwd())
57-
current_match = current_dir if current_dir.endswith("/") else f"{current_dir}/"
58-
allowed_re = str(conf["path"][0])
59-
denied_re = str(conf["path"][1][:-1])
60-
current_allowed = bool(re.findall(allowed_re, current_match))
61-
current_denied = bool(re.findall(denied_re, current_match)) if denied_re else False
62-
current_status = "allowed" if current_allowed and not current_denied else "denied"
49+
path_acl = conf.get("path", ["", ""])
50+
allow = path_acl[0] if len(path_acl) > 0 else ""
51+
deny = path_acl[1] if len(path_acl) > 1 else ""
52+
allowed_roots = sec_policy._split_path_acl_entries(allow)
53+
denied_roots = sec_policy._split_path_acl_entries(deny)
54+
current_allowed = sec_policy._is_path_allowed(
55+
current_dir, allowed_roots, denied_roots
56+
)
57+
current_status = "allowed" if current_allowed else "denied"
6358

6459
sys.stdout.write("Path Policy\n")
6560
sys.stdout.write("-----------\n")

lshell/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import sys
77
import uuid
88

9-
from lshell import policy as policy_mode
9+
from lshell.config import diagnostics as policy_mode
1010
from lshell import systemsetup as system_setup
1111
from lshell import hardeninit as harden_init
1212
from lshell import audit
1313
from lshell import containment
14-
from lshell.checkconfig import CheckConfig
14+
from lshell.config.runtime import CheckConfig
1515
from lshell.shellcmd import LshellTimeOut, ShellCmd
1616

1717

0 commit comments

Comments
 (0)