25 Commits

Author SHA1 Message Date
2ee9588fb9 Update filelock and virtualenv
All checks were successful
Trivy Scan / security-scan (push) Successful in 26s
2026-01-15 17:06:08 +00:00
a2720e245f Change trivy.json output path 2026-01-15 16:47:54 +00:00
8e3404bd51 Rework the trivy scan job 2026-01-15 16:45:21 +00:00
5ff1e935a3 Fix trivy scan volume path to be explicit 2026-01-15 16:39:40 +00:00
7fafb1fa8e Fix trivy scan volume path 2026-01-15 16:36:23 +00:00
f87eb5f438 Fix trivy scan volume path 2026-01-15 16:22:47 +00:00
1c3025b2d6 Fix docker command to trivy scan 2026-01-15 16:19:08 +00:00
4980903572 Fix trivy.json output path 2026-01-15 16:13:48 +00:00
a7183b3286 Fix trivy.json output path 2026-01-15 16:09:17 +00:00
984aeccfdd Fix trivy fs path 2026-01-15 16:05:51 +00:00
dc6e7840c7 Modify severity level on trivy-scan workflow 2026-01-15 15:48:41 +00:00
0464982b94 Add trivy-scan workflow 2026-01-15 15:41:29 +00:00
4a4cb8183f Merge pull request 'Add wrapper mode' (#4) from ch_wrapper into main
Reviewed-on: #4
2025-12-30 17:31:45 +00:00
20a0dca080 Add wrapper mode, update README, version bump 0.3.0
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 2m7s
2025-12-30 17:27:15 +00:00
7c391b8dbc Fix logo path in README 2025-12-23 16:18:18 +00:00
aafad81bb6 Merge pull request 'Make save operation transactional' (#3) from transaction_patch into main
Reviewed-on: #3
2025-12-23 16:15:46 +00:00
9658f534ea Run save inside a single transaction to avoid partial writes when permission checks fail or state creation errors occur
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 59s
2025-12-23 16:07:46 +00:00
5af28d21ca Add .python-version to .gitignore 2025-12-21 14:03:52 +00:00
b0395a432f Merge pull request 'Patch dependecies' (#2) from fix_dependencies_notation into main
Reviewed-on: #2
2025-12-21 10:25:30 +00:00
603e2ac0c6 Patch dependecies
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 1m0s
2025-12-21 10:23:59 +00:00
9c915576e9 Merge pull request 'Add tab completion, update README, version bump 0.2.0' (#1) from relax_dependencies into main
Reviewed-on: #1
2025-12-21 10:03:33 +00:00
96970b6963 Rework lint-and-security workflow to add Poetry and the export plugin to work with pip-audit
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 59s
2025-12-21 10:01:05 +00:00
5353310e15 Edit workflow to run pip-audit against a poetry export file
Some checks failed
Lint & Security / precommit-and-security (pull_request) Failing after 1m11s
2025-12-21 09:54:08 +00:00
e8f63386bb Add filelock ^3.20.1 in order to get away from CVE-2025-68146
Some checks failed
Lint & Security / precommit-and-security (pull_request) Failing after 47s
2025-12-21 09:40:56 +00:00
e0ec2ce60a Add tab completion, update README, version bump 0.2.0
Some checks failed
Lint & Security / precommit-and-security (pull_request) Failing after 1m37s
2025-12-21 09:29:21 +00:00
8 changed files with 389 additions and 80 deletions

View File

@@ -22,8 +22,15 @@ jobs:
- name: Run pre-commit hooks - name: Run pre-commit hooks
run: pre-commit run --all-files --color always run: pre-commit run --all-files --color always
- name: Install Poetry
run: |
pip install poetry
poetry self add poetry-plugin-export
- name: Install pip-audit - name: Install pip-audit
run: pip install pip-audit run: pip install pip-audit
- name: Run pip-audit - name: Audit dependencies (Poetry lockfile)
run: pip-audit run: |
poetry export -f requirements.txt --without-hashes \
| pip-audit -r /dev/stdin

View File

@@ -0,0 +1,61 @@
---
name: Trivy Scan
on:
schedule:
- cron: 17 8 * * *
workflow_dispatch:
jobs:
security-scan:
runs-on: running-man
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Trivy scan via Docker
id: trivy
continue-on-error: true
run: |
docker run --rm \
--volumes-from "$HOSTNAME" \
aquasec/trivy:latest \
fs /workspace/guardutils/chguard \
--scanners vuln \
--pkg-types library \
--include-dev-deps \
--severity MEDIUM,HIGH,CRITICAL \
--ignore-unfixed \
--format json \
--output /workspace/guardutils/chguard/trivy.json \
--exit-code 1
- name: Notify Node-RED on vulnerabilities
if: steps.trivy.outcome == 'failure'
run: |
jq -r '
{
repo: "guardutils/chguard",
summary: (
"Total: " +
((.Results[].Vulnerabilities | length) | tostring)
),
vulnerabilities: [
.Results[].Vulnerabilities[] | {
library: .PkgName,
cve: .VulnerabilityID,
severity: .Severity,
installed: .InstalledVersion,
fixed: .FixedVersion,
title: .Title,
url: .PrimaryURL
}
]
}
' trivy.json \
| curl -s -X POST https://nodered.sysmd.uk/trivy-alert \
-H "Content-Type: application/json" \
--data-binary @-
- name: Fail workflow if vulnerabilities found
if: steps.trivy.outcome == 'failure'
run: exit 1

2
.gitignore vendored
View File

@@ -85,7 +85,7 @@ ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

View File

@@ -5,7 +5,7 @@
# chguard # chguard
<div align="center"> <div align="center">
<img src="chguard.png" alt="chguard logo" width="256" /> <img src="https://git.sysmd.uk/guardutils/chguard/raw/branch/main/chguard.png" alt="chguard logo" width="256" />
</div> </div>
@@ -30,6 +30,31 @@ A single confirmation prompt at the end of a restore (default: **No**).
### Dry-run mode ### Dry-run mode
Preview restore operations without prompting or applying changes. Preview restore operations without prompting or applying changes.
### Wrapper mode (automatic snapshots)
`chguard` can also run as a wrapper around ownership and permission commands.
In this mode, `chguard` automatically saves a snapshot before the command runs, so the user can easily restore the previous state if needed.
#### Supported commands
Wrapper mode is intentionally limited to commands that modify filesystem metadata only:
* `chown`
* `chmod`
* `chgrp`
Other commands are rejected to avoid giving a _false sense of protection_.
#### Automatic snapshot names
Snapshots created in wrapper mode are named automatically, for example:
```
auto-20251230-161301
```
Auto-generated snapshots are visually distinguished in the output so they are easy to identify.
### Scope control ### Scope control
Restore: Restore:
* both ownership and permissions (default) * both ownership and permissions (default)
@@ -55,7 +80,6 @@ Restore:
It only concerns itself with **ownership** and **permissions**. It only concerns itself with **ownership** and **permissions**.
## Installation ## Installation
### From GuardUtils package repo ### From GuardUtils package repo
@@ -179,6 +203,16 @@ chguard --restore app-baseline --permissions
chguard --restore app-baseline --owner chguard --restore app-baseline --owner
``` ```
### Wrapper mode
Use `--` to separate `chguard` arguments from the wrapped command:
```
chguard -- chown user:group file
chguard -- chmod 755 file
chguard -- chgrp staff file
```
## Privilege model ## Privilege model
`chguard` never escalates privileges automatically `chguard` never escalates privileges automatically
@@ -198,6 +232,16 @@ Snapshots are stored in a local SQLite database containing:
Usernames and permission strings are resolved only for display. Usernames and permission strings are resolved only for display.
### TAB completion
Add this to your `.bashrc`
```
eval "$(register-python-argcomplete chguard)"
```
And then
```
source ~/.bashrc
```
## pre-commit ## pre-commit
This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks). This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks).

View File

@@ -1,11 +1,14 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import argcomplete
import importlib.metadata
import os import os
import sys import sys
import stat import stat
import pwd import pwd
import grp import grp
import subprocess
from collections import Counter, defaultdict from collections import Counter, defaultdict
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
@@ -27,8 +30,14 @@ from chguard.restore import plan_restore, apply_restore
from chguard.util import normalize_root from chguard.util import normalize_root
def get_version():
try:
return importlib.metadata.version("chguard")
except importlib.metadata.PackageNotFoundError:
return "unknown"
def _uid_to_name(uid: int) -> str: def _uid_to_name(uid: int) -> str:
"""Return username for uid, or uid as string if unknown."""
try: try:
return pwd.getpwuid(uid).pw_name return pwd.getpwuid(uid).pw_name
except KeyError: except KeyError:
@@ -36,7 +45,6 @@ def _uid_to_name(uid: int) -> str:
def _gid_to_name(gid: int) -> str: def _gid_to_name(gid: int) -> str:
"""Return group name for gid, or gid as string if unknown."""
try: try:
return grp.getgrgid(gid).gr_name return grp.getgrgid(gid).gr_name
except KeyError: except KeyError:
@@ -44,12 +52,10 @@ def _gid_to_name(gid: int) -> str:
def _format_owner(uid: int, gid: int) -> str: def _format_owner(uid: int, gid: int) -> str:
"""Format uid/gid as username:group."""
return f"{_uid_to_name(uid)}:{_gid_to_name(gid)}" return f"{_uid_to_name(uid)}:{_gid_to_name(gid)}"
def _mode_to_rwx(mode: int) -> str: def _mode_to_rwx(mode: int) -> str:
"""Convert numeric mode to rwx-style permissions."""
bits = ( bits = (
stat.S_IRUSR, stat.S_IRUSR,
stat.S_IWUSR, stat.S_IWUSR,
@@ -79,10 +85,33 @@ def _mode_to_rwx(mode: int) -> str:
def _is_root() -> bool: def _is_root() -> bool:
"""Return True if running as root."""
return os.geteuid() == 0 return os.geteuid() == 0
def complete_state_names(prefix, parsed_args, **kwargs):
try:
conn = connect(
Path(parsed_args.db).expanduser().resolve()
if parsed_args.db
else None
)
rows = conn.execute("SELECT name FROM states").fetchall()
return [name for (name,) in rows if name.startswith(prefix)]
except Exception:
return []
def _extract_paths_from_command(cmd: list[str]) -> list[Path]:
paths = []
for arg in cmd:
if arg.startswith("-"):
continue
p = Path(arg)
if p.exists():
paths.append(p.resolve())
return paths
def main() -> None: def main() -> None:
""" """
Entry point for the CLI. Entry point for the CLI.
@@ -94,57 +123,205 @@ def main() -> None:
- Symlinks are skipped during scanning - Symlinks are skipped during scanning
""" """
wrapper_cmd = None
if "--" in sys.argv:
idx = sys.argv.index("--")
wrapper_cmd = sys.argv[idx + 1 :]
sys.argv = sys.argv[:idx]
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="chguard", prog="chguard",
description="Snapshot and restore filesystem ownership and permissions.", description="Snapshot and restore filesystem ownership and permissions.",
) )
actions = parser.add_mutually_exclusive_group(required=True) parser = argparse.ArgumentParser(
actions.add_argument("--save", metavar="PATH", help="Save state for PATH") prog="chguard",
description="Snapshot and restore filesystem ownership and permissions.",
epilog=(
"Wrapper mode:\n"
" chguard -- chown [OPTIONS] PATH...\n"
" chguard -- chmod [OPTIONS] PATH...\n"
" chguard -- chgrp [OPTIONS] PATH...\n\n"
"In wrapper mode, chguard automatically saves a snapshot of ownership\n"
"and permissions for the affected paths before running the command.\n"
"Only chown, chmod, and chgrp are supported."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
actions = parser.add_mutually_exclusive_group(required=wrapper_cmd is None)
parser.add_argument(
"--version",
action="version",
version=f"chguard {get_version()}",
)
actions.add_argument( actions.add_argument(
"--restore", action="store_true", help="Restore a saved state" "--save",
) metavar="PATH",
help="Save state for PATH",
).completer = argcomplete.FilesCompleter()
actions.add_argument( actions.add_argument(
"--list", action="store_true", help="List saved states" "--restore",
action="store_true",
help="Restore a saved state",
) )
actions.add_argument( actions.add_argument(
"--delete", metavar="STATE", help="Delete a saved state" "--list",
action="store_true",
help="List saved states",
)
actions.add_argument(
"--delete",
metavar="STATE",
help="Delete a saved state",
).completer = complete_state_names
# positional STATE
parser.add_argument(
"state",
nargs="?",
help="State name (required with --restore)",
).completer = complete_state_names
parser.add_argument(
"--name",
help="State name (required with --save)",
) )
parser.add_argument( parser.add_argument(
"state", nargs="?", help="State name (required with --restore)" "--overwrite",
) action="store_true",
parser.add_argument("--name", help="State name (required with --save)") help="Overwrite existing state",
parser.add_argument(
"--overwrite", action="store_true", help="Overwrite existing state"
) )
parser.add_argument( parser.add_argument(
"--permissions", action="store_true", help="Restore MODE only" "--permissions",
) action="store_true",
parser.add_argument( help="Restore MODE only",
"--owner", action="store_true", help="Restore OWNER only"
) )
parser.add_argument( parser.add_argument(
"--dry-run", action="store_true", help="Preview only; do not apply" "--owner",
) action="store_true",
parser.add_argument( help="Restore OWNER only",
"--yes", action="store_true", help="Apply without confirmation"
) )
parser.add_argument("--root", metavar="PATH", help="Override restore root")
parser.add_argument( parser.add_argument(
"--exclude", action="append", default=[], help="Exclude path prefix" "--dry-run",
action="store_true",
help="Preview only; do not apply",
) )
parser.add_argument("--db", metavar="PATH", help="Override database path")
parser.add_argument(
"--yes",
action="store_true",
help="Apply without confirmation",
)
parser.add_argument(
"--root",
metavar="PATH",
help="Override restore root",
).completer = argcomplete.FilesCompleter()
parser.add_argument(
"--exclude",
action="append",
default=[],
help="Exclude path prefix",
).completer = argcomplete.FilesCompleter()
parser.add_argument(
"--db",
metavar="PATH",
help="Override database path",
).completer = argcomplete.FilesCompleter()
argcomplete.autocomplete(parser)
args = parser.parse_args() args = parser.parse_args()
if wrapper_cmd is not None:
if not wrapper_cmd:
raise SystemExit("No command provided after '--'")
cmd = Path(wrapper_cmd[0]).name
if cmd not in ("chown", "chmod", "chgrp"):
raise SystemExit(
"Wrapper mode only supports chown, chmod, and chgrp"
)
console = Console() console = Console()
conn = connect(Path(args.db).expanduser().resolve() if args.db else None) conn = connect(Path(args.db).expanduser().resolve() if args.db else None)
init_db(conn) init_db(conn)
if wrapper_cmd:
paths = _extract_paths_from_command(wrapper_cmd)
if paths:
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
with conn:
root_path = str(Path(paths[0]).resolve())
state_id = create_state(
conn, auto_name, root_path, os.getuid(), commit=False
)
for path in paths:
if path.is_dir():
for entry in scan_tree(path):
if entry.uid == 0 and not _is_root():
raise SystemExit(
"This command affects root-owned files.\n"
"Please re-run with sudo."
)
conn.execute(
"""
INSERT INTO entries (state_id, path, type, mode, uid, gid)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
state_id,
entry.path,
entry.type,
entry.mode,
entry.uid,
entry.gid,
),
)
else:
st = path.lstat()
if st.st_uid == 0 and not _is_root():
raise SystemExit(
"This command affects root-owned files.\n"
"Please re-run with sudo."
)
conn.execute(
"""
INSERT INTO entries (state_id, path, type, mode, uid, gid)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
state_id,
str(path),
"file",
stat.S_IMODE(st.st_mode),
st.st_uid,
st.st_gid,
),
)
console.print(
f"Saved pre-command snapshot: [cyan]{auto_name}[/cyan]"
)
proc = subprocess.run(wrapper_cmd)
sys.exit(proc.returncode)
if args.list: if args.list:
rows = conn.execute( rows = conn.execute(
"SELECT name, root_path, created_at FROM states ORDER BY created_at DESC" "SELECT name, root_path, created_at FROM states ORDER BY created_at DESC"
@@ -157,7 +334,10 @@ def main() -> None:
for name, root, created in rows: for name, root, created in rows:
dt = datetime.fromisoformat(created) dt = datetime.fromisoformat(created)
ts = dt.strftime("%Y-%m-%d %H:%M:%S %z") ts = dt.strftime("%Y-%m-%d %H:%M:%S %z")
console.print(f"{name}\t{root}\t[dim]{ts}[/dim]") if name.startswith("auto-"):
console.print(f"[cyan]{name}[/cyan]\t{root}\t{ts}")
else:
console.print(f"{name}\t{root}\t{ts}")
return return
if args.delete: if args.delete:
@@ -172,14 +352,19 @@ def main() -> None:
root = normalize_root(args.save) root = normalize_root(args.save)
try:
with conn: # start transaction
if state_exists(conn, args.name): if state_exists(conn, args.name):
if not args.overwrite: if not args.overwrite:
raise SystemExit( raise SystemExit(
f"State '{args.name}' already exists (use --overwrite)" f"State '{args.name}' already exists (use --overwrite)"
) )
delete_state(conn, args.name) # if the new save fails, this delete_state step will also roll back
delete_state(conn, args.name, commit=False)
state_id = create_state(conn, args.name, str(root), os.getuid()) state_id = create_state(
conn, args.name, str(root), os.getuid(), commit=False
)
# Abort early if root-owned files exist and user is not root. # Abort early if root-owned files exist and user is not root.
# This prevents creating snapshots that cannot be meaningfully restored. # This prevents creating snapshots that cannot be meaningfully restored.
@@ -205,10 +390,12 @@ def main() -> None:
), ),
) )
conn.commit()
console.print(f"Saved state '{args.name}' for {root}") console.print(f"Saved state '{args.name}' for {root}")
return return
except SystemExit:
raise
if args.restore: if args.restore:
if not args.state: if not args.state:
parser.error("STATE is required with --restore") parser.error("STATE is required with --restore")
@@ -263,7 +450,7 @@ def main() -> None:
] = f"{_format_owner(bu, bg)}{_format_owner(au, ag)}" ] = f"{_format_owner(bu, bg)}{_format_owner(au, ag)}"
counts["owner"] += 1 counts["owner"] += 1
if au != current_uid: if ch.path.stat().st_uid != current_uid:
needs_root = True needs_root = True
elif ch.kind == "mode" and restore_permissions: elif ch.kind == "mode" and restore_permissions:

View File

@@ -61,18 +61,27 @@ def state_exists(conn: sqlite3.Connection, name: str) -> bool:
def create_state( def create_state(
conn: sqlite3.Connection, name: str, root_path: str, created_by_uid: int conn: sqlite3.Connection,
name: str,
root_path: str,
created_by_uid: int,
*,
commit: bool = True,
) -> int: ) -> int:
cur = conn.execute( cur = conn.execute(
"INSERT INTO states (name, root_path, created_at, created_by_uid) VALUES (?, ?, ?, ?)", "INSERT INTO states (name, root_path, created_at, created_by_uid) VALUES (?, ?, ?, ?)",
(name, root_path, utc_now_iso(), created_by_uid), (name, root_path, utc_now_iso(), created_by_uid),
) )
if commit:
conn.commit() conn.commit()
return int(cur.lastrowid) return int(cur.lastrowid)
def delete_state(conn: sqlite3.Connection, name: str) -> int: def delete_state(
conn: sqlite3.Connection, name: str, commit: bool = True
) -> int:
cur = conn.execute("DELETE FROM states WHERE name = ?", (name,)) cur = conn.execute("DELETE FROM states WHERE name = ?", (name,))
if commit:
conn.commit() conn.commit()
return cur.rowcount return cur.rowcount

16
poetry.lock generated
View File

@@ -38,13 +38,13 @@ files = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.20.1" version = "3.20.3"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
] ]
[[package]] [[package]]
@@ -267,18 +267,18 @@ files = [
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.35.4" version = "20.36.1"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"},
{file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"},
] ]
[package.dependencies] [package.dependencies]
distlib = ">=0.3.7,<1" distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4" filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}
platformdirs = ">=3.9.1,<5" platformdirs = ">=3.9.1,<5"
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
@@ -289,4 +289,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
content-hash = "49f77d614e46109e49e997fa270cb7093d6f7e7d258e370c4eddd4354c20437f" content-hash = "4a5c993fcc16fe3739c43eb00bed750ce0803d45e37c7a786aa0b83bb4930267"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "chguard" name = "chguard"
version = "0.1.0" version = "0.3.1"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = ["Marco D'Aleo <marco@marcodaleo.com>"] authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@@ -12,7 +12,8 @@ repository = "https://git.sysmd.uk/guardutils/chguard"
python = ">=3.10,<4.0" python = ">=3.10,<4.0"
rich = ">=12" rich = ">=12"
argcomplete = ">=2" argcomplete = ">=2"
platformdirs = "^4.5.1" platformdirs = ">=4.5.1"
filelock = ">=3.20.1"
[tool.poetry.scripts] [tool.poetry.scripts]
chguard = "chguard.cli:main" chguard = "chguard.cli:main"