Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2

updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
github-actions:
patterns:
- "*"
27 changes: 22 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
shellcheck:
name: Shell checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Install shellcheck
run: |
Expand All @@ -29,8 +36,18 @@ jobs:
fi
shellcheck -x "${files[@]}"

- name: Check shell formatting (shfmt)
- name: Download, verify, and run shfmt
env:
SHFMT_VERSION: 3.13.1
SHFMT_SHA256: fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1
run: |
curl -sS https://webinstall.dev/shfmt | bash
export PATH="$HOME/.local/bin:$PATH"
shfmt -d proxmox-manager.sh
set -euo pipefail
shfmt_path="${RUNNER_TEMP}/shfmt"
curl --fail --location --silent --show-error \
--proto '=https' --tlsv1.2 \
--retry 3 \
"https://github.com/mvdan/sh/releases/download/v${SHFMT_VERSION}/shfmt_v${SHFMT_VERSION}_linux_amd64" \
--output "${shfmt_path}"
printf '%s %s\n' "${SHFMT_SHA256}" "${shfmt_path}" | sha256sum --check --strict
chmod 0755 "${shfmt_path}"
"${shfmt_path}" -d proxmox-manager.sh
35 changes: 26 additions & 9 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,47 @@ on:
permissions:
contents: read

concurrency:
group: gitleaks-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
scan:
name: Secret scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false

- name: Install gitleaks CLI
- name: Download and verify gitleaks CLI
env:
GITLEAKS_VERSION: 8.30.1
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
run: |
GITLEAKS_VERSION=8.21.2
curl -sSfL \
set -euo pipefail
archive="${RUNNER_TEMP}/gitleaks.tar.gz"
install_dir="${RUNNER_TEMP}/gitleaks-bin"
curl --fail --location --silent --show-error \
--proto '=https' --tlsv1.2 \
--retry 3 \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| sudo tar -xz -C /usr/local/bin gitleaks
--output "${archive}"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${archive}" | sha256sum --check --strict
mkdir -p "${install_dir}"
tar -xzf "${archive}" -C "${install_dir}" gitleaks
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"

- name: Run gitleaks (workspace scan)
- name: Run gitleaks (history scan)
run: |
gitleaks detect --redact --source . \
gitleaks git --redact --log-opts="--all" \
--config .gitleaks.toml \
--report-format=sarif --report-path=results.sarif --exit-code=1
--report-format=sarif --report-path=results.sarif --exit-code=1 .

- name: Upload SARIF report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: gitleaks-results
path: results.sarif
25 changes: 18 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@ on:
tags:
- "v*"

permissions:
contents: write
permissions: {}

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Publish release
permissions:
contents: write # Required to create a GitHub release and upload its asset.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: proxmox-manager.sh
generate_release_notes: false
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" proxmox-manager.sh \
--verify-tag \
--title "$GITHUB_REF_NAME" \
--notes ""
6 changes: 0 additions & 6 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,3 @@ title = "Gitleaks config for proxmox-manager"

[extend]
useDefault = true

[allowlist]
description = "Ignore example/placeholder values in documentation"
paths = [
"README.md",
]
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Security
- Configuration files are now parsed as allowlisted data instead of being executed with `source`.
- Log files must be private, owner-controlled regular files; unsafe paths and symlinks are rejected.
- The system-wide `pman` command is installed as an atomic root-owned copy instead of a symlink to a potentially user-writable checkout.
- GitHub Actions are commit-SHA pinned, remote tool downloads are checksum-verified, workflow permissions are minimized, and checkout credentials are not persisted.
- User-controlled status and message text is no longer interpreted as `printf %b` escape sequences.

### Fixed
- VM IP lookup now accepts the native QEMU Guest Agent list payload from `qm agent ... network-get-interfaces`
as well as the earlier wrapped shape, preventing parser errors on hosts that return a bare JSON array.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Check out how others are using `proxmox-manager`:
| 📦 | **Snapshot Management** | List, create, rollback, delete — with name validation and snapshot preview before destructive actions |
| 🖱️ | **SPICE Integration** | Enable SPICE for VMs and retrieve `.vv` connection files. Auto-launches `virt-viewer` when installed |
| 🤖 | **Automation-Ready** | `--json` output, `--filter` by status, `--name` ERE filter, `--force` mode, structured logging via `LOG_FILE` |
| ⚙️ | **Config File** | Persistent defaults via `/etc/pmanrc` or `~/.pmanrc` — CLI flags always win |
| ⚙️ | **Config File** | Persistent allowlisted defaults via `/etc/pmanrc` or `~/.pmanrc` — CLI flags always win |

---

Expand Down Expand Up @@ -125,7 +125,7 @@ chmod +x proxmox-manager.sh
./install_dependencies.sh
```

Installs a symlink to `/usr/local/bin/pman` so you can call `pman` from anywhere on the host.
Installs an atomic, root-owned copy at `/usr/local/bin/pman` so later edits to the checkout cannot change the privileged executable.

---

Expand Down Expand Up @@ -198,9 +198,11 @@ CLI flags are applied last and always win over config file values.
# ~/.pmanrc — example
STOP_TIMEOUT=120 # stop timeout in seconds (default: 60)
LOG_FILE="/var/log/proxmox-manager.log" # structured log file (empty = disabled)
PROXMOX_MANAGER_SPICE_ADDR="192.168.1.10" # SPICE bind address (default: 127.0.0.1)
PROXMOX_MANAGER_SPICE_ADDR="spice.example.invalid" # SPICE bind address
```

Configuration files are parsed as data, not executed as shell code. Only the three settings shown above are accepted. When enabled, `LOG_FILE` must be an absolute, non-symlinked regular file owned by the current user with mode `0600`; its parent directory must also be owner-controlled and not group/world-writable.

### Shell Completions

```bash
Expand Down
35 changes: 28 additions & 7 deletions install_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,45 @@ else
fi

# ---------------------------------------------------------------------------
# Install pman — global symlink in /usr/local/bin
# Install pman — root-owned copy in /usr/local/bin
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
PMAN_SRC="${SCRIPT_DIR}/proxmox-manager.sh"
PMAN_DIR="/usr/local/bin"
PMAN_DST="/usr/local/bin/pman"

if [[ ! -f "$PMAN_SRC" ]]; then
_err "proxmox-manager.sh not found at ${PMAN_SRC}. Run install_dependencies.sh from the repo root."
exit 1
fi

chmod +x "$PMAN_SRC"
if [[ ! -d "$PMAN_DIR" || -L "$PMAN_DIR" ]]; then
_err "${PMAN_DIR} must be an existing regular directory."
exit 1
fi

if [[ -L "$PMAN_DST" && "$(readlink -f "$PMAN_DST")" == "$(readlink -f "$PMAN_SRC")" ]]; then
_log "pman already installed: ${PMAN_DST}"
else
ln -sf "$PMAN_SRC" "$PMAN_DST"
_ok "Installed: pman -> ${PMAN_SRC}"
PMAN_DIR_OWNER="$(stat -Lc '%u' -- "$PMAN_DIR" 2>/dev/null || printf 'invalid')"
PMAN_DIR_MODE="$(stat -Lc '%a' -- "$PMAN_DIR" 2>/dev/null || printf 'invalid')"
if [[ "$PMAN_DIR_OWNER" != "0" || ! "$PMAN_DIR_MODE" =~ ^[0-7]{3,4}$ ]] || ((8#$PMAN_DIR_MODE & 8#022)); then
_err "${PMAN_DIR} must be root-owned and not group/world-writable."
exit 1
fi

PMAN_TMP="$(mktemp -p "$PMAN_DIR" '.pman.XXXXXX')" || {
_err "Could not create a temporary install file in ${PMAN_DIR}."
exit 1
}

if ! install -o root -g root -m 0755 "$PMAN_SRC" "$PMAN_TMP" || ! cmp -s "$PMAN_SRC" "$PMAN_TMP"; then
rm -f -- "$PMAN_TMP"
_err "Failed to stage a verified pman executable."
exit 1
fi
if ! mv -fT -- "$PMAN_TMP" "$PMAN_DST"; then
rm -f -- "$PMAN_TMP"
_err "Failed to install ${PMAN_DST}."
exit 1
fi
_ok "Installed root-owned executable: ${PMAN_DST}"

_ok "Done. All optional dependencies are available. Run 'pman' to start."
Loading
Loading