Skip to content

Commit fb327ac

Browse files
committed
fix: security fixes / tests / README
1 parent 26f631d commit fb327ac

22 files changed

Lines changed: 713 additions & 182 deletions

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Test on ${{ matrix.os }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Ensure scripts are executable
23+
run: |
24+
find . -type f -name "*.sh" -exec chmod +x {} \;
25+
26+
- name: Syntax check shell scripts
27+
run: |
28+
set -e
29+
while IFS= read -r script; do
30+
bash -n "$script"
31+
done < <(find . -type f -name "*.sh")
32+
33+
- name: Run full test suite
34+
run: ./run-tests.sh

README.md

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# WebDev Backup Tool
22

3+
<!-- Optional CI badge (replace USER/REPO with your GitHub org/repo) -->
4+
[![CI](https://github.com/USER/REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/USER/REPO/actions/workflows/ci.yml)
5+
36
<p align="center">
47
<img src="assets/og-image.png" alt="WebDev Backup Tool" width="1200" />
58
</p>
@@ -34,7 +37,7 @@ You can also run `./setup-alias.sh` from the repo to add the alias for you.
3437

3538
## Features
3639

37-
- **Backup**: Multi-directory, full/incremental/differential, quick backup (default from menu), compression (gzip/pigz), excludes `node_modules`. Each `.tar.gz` is self-contained: extracting creates a single top-level folder with everything inside. Quick backup shows live per-folder progress (same dashboard as interactive).
40+
- **Backup**: Multi-directory, full/incremental/differential, quick backup (default from menu), compression (gzip/pigz). **`node_modules` is never included** in backups to save space; you can rebuild dependencies after restore with `yarn install` or `npm install`. Each `.tar.gz` is self-contained: extracting creates a single top-level folder with everything inside. Quick backup shows live per-folder progress (same dashboard as interactive).
3841
- **Restore**: With integrity validation (tar + SHA256); optional `--skip-verify`
3942
- **Reporting**: HTML reports, email, charts (gnuplot), backup comparison
4043
- **Security**: Permissions, secrets handling, security audit script
@@ -70,28 +73,39 @@ Tests run with no config (they use `test/` and `test-projects/`).
7073

7174
```bash
7275
./run-tests.sh # Unit + integration + security audit
73-
./run-tests.sh --unit # Unit only (32 tests)
74-
./run-tests.sh --integration # Integration only (9 tests)
76+
./run-tests.sh --unit # Unit only (38 tests)
77+
./run-tests.sh --integration # Integration only (10 tests)
7578
./run-tests.sh --security # Security audit only
7679
```
7780

78-
**Coverage (41 tests):**
81+
**Coverage (48 tests):**
7982

8083
| Category | Tests | What's covered |
8184
|----------|-------|----------------|
82-
| **Unit (32)** | `format_size` (0 B through GB), `sanitize_input` (basic + strict), `validate_path` (traversal, injection, empty, relative, absolute), `detect_os`, `get_os_version_display`, `get_file_size_bytes` (file + missing), `calculate_checksum` (consistency + SHA256 length), `check_required_tools` (present + missing), `format_time` (seconds/minutes/hours), `capitalize`, `verify_directory` (exists + missing), `find_projects`, `verify_backup` (valid + corrupted archive) | Core utility functions, edge cases, and error paths |
83-
| **Integration (9)** | Backup dry-run, full backup, backup-file existence, node_modules exclusion, archive integrity (`verify_backup`), source-file presence in archive, restore dry-run, incremental backup, config with RUNNING_TESTS | End-to-end backup/restore workflow and configuration |
85+
| **Unit (38)** | `format_size` (0 B through GB), `sanitize_input` (basic + strict), `validate_path` (traversal, injection, empty, relative, absolute), `detect_os`, `get_os_version_display`, `get_file_size_bytes` (file + missing), `calculate_checksum` (consistency + SHA256 length), `check_required_tools` (present + missing), `format_time` (seconds/minutes/hours), `capitalize`, `verify_directory` (exists + missing), `find_projects`, `verify_backup` (valid + corrupted archive), `is_safe_config_literal` checks, and `sanitize_cron_backup_options` allow/reject cases | Core utility functions, edge cases, and error paths |
86+
| **Integration (10)** | Backup dry-run, full backup, backup-file existence, node_modules exclusion, archive integrity (`verify_backup`), source-file presence in archive, **.env and lockfiles included**, restore dry-run, incremental backup, config with RUNNING_TESTS | End-to-end backup/restore workflow and configuration |
8487
| **Security** | File permissions, sensitive files in git, hardcoded credentials, eval usage, temp file handling | Static analysis of common vulnerabilities |
8588

8689
You can also run `./test-backup.sh` (with `--quick`, `--unit`, or `--integration`) or `npm test`.
8790

88-
**Platform verification (macOS, Linux, WSL2):** The same test suite and scripts are supported on all three. To verify on your platform:
91+
**Platform verification (macOS, Linux, WSL2):** The same test suite and scripts run on all three. Ubuntu is the primary Linux target.
92+
93+
| Platform | How to verify |
94+
|----------|---------------|
95+
| **macOS** | `./run-tests.sh` (all 42 tests), `./backup.sh --quick --dry-run`, `./verify-implementation.sh` |
96+
| **Linux (Ubuntu)** | Same commands; uses GNU tools (`stat -c`, `date -d`, etc.) |
97+
| **WSL2** | Same as Linux; use `/mnt/c`, `/mnt/d` etc. for Windows drives (see **Paths by platform** below) |
98+
99+
- **Compatibility check:** `./verify-implementation.sh` confirms config, first-run, and OS-specific paths (e.g. `get_file_size_bytes`, `run_with_timeout`, tar flags). It reports macOS or Linux and suggests next steps.
100+
- **Quick smoke test:** `./backup.sh --quick --dry-run` shows the backup dashboard and a simulated run without writing files.
101+
102+
The app uses `uname -s` to choose the right commands (e.g. BSD vs GNU `find`, `stat` vs `du`, `date -r` vs `date -d`).
103+
104+
## Exclusions and what’s included
89105

90-
- **macOS / Linux / WSL2:** Run `./run-tests.sh` (no config required; uses test directories). All 41 tests should pass.
91-
- **Compatibility check:** Run `./verify-implementation.sh` to confirm config, first-run, and OS-specific paths (e.g. `get_file_size_bytes`, `run_with_timeout`, tar flags). It reports macOS or Linux and suggests next steps.
92-
- **Quick smoke test:** `./backup.sh --quick --dry-run` should show the backup dashboard and a simulated run without writing files.
106+
- **`node_modules`** – Excluded from all backups (full, incremental, differential, and quick) to **save space**. Dependencies are easy to rebuild after restore: run `yarn install` or `npm install` in each project directory. Exclusion is verified by the integration test suite.
93107

94-
On WSL2, use `/mnt/<letter>/` for Windows drives (see **Paths by platform** in Configuration). The app uses `uname -s` to choose the right commands (e.g. BSD vs GNU `find`, `stat` vs `du`).
108+
- **Always included** (so you can reconstruct the app after restore): **`.env`** (and common variants like `.env.local`), **`yarn.lock`**, and **npm lockfiles** (`package-lock.json`, `npm-shrinkwrap.json`) are *not* excluded. With these in the backup, a restore plus `yarn install` or `npm install` reproduces the same dependency tree and environment.
95109

96110
## Configuration
97111

@@ -128,6 +142,37 @@ All command-line options pass through to the alias. Dry-run only checks that sou
128142
2. **`./secure-secrets.sh`** – set up credential storage
129143
3. **`./security-audit.sh`** – checks permissions, sensitive files in git, hardcoded credentials, eval, temp files (also runs with `./run-tests.sh`)
130144

145+
## Pull Request Checklist
146+
147+
Use this checklist before opening or merging a PR:
148+
149+
- [ ] Ran `./run-tests.sh` locally and all suites passed.
150+
- [ ] Ran `./security-audit.sh` and it reported no issues.
151+
- [ ] Verified on at least one target platform (`macOS` or `Ubuntu/WSL2`).
152+
- [ ] If shell scripts changed, ensured `bash -n` passes.
153+
- [ ] Updated `README.md` or docs for any behavior/CLI changes.
154+
- [ ] Confirmed no secrets were added (especially `secrets.sh`, `.env`, credentials).
155+
156+
CI (`.github/workflows/ci.yml`) enforces syntax checks and full tests on macOS and Ubuntu.
157+
158+
## Contributing
159+
160+
Before opening a PR, run the local quality gate:
161+
162+
```bash
163+
./run-tests.sh && ./security-audit.sh
164+
```
165+
166+
Recommended when changing shell scripts:
167+
168+
```bash
169+
find . -type f -name "*.sh" -exec bash -n {} \;
170+
```
171+
172+
If your change affects platform-specific behavior, validate at least one of:
173+
- macOS
174+
- Ubuntu Linux (or WSL2 Ubuntu)
175+
131176
**What's hardened:**
132177
- All temp files use `mktemp` (no `/tmp/` predictable paths in active code)
133178
- `umask 027` in utils, encryption, and setup scripts
@@ -144,6 +189,7 @@ Full details: [docs/SECURITY_REVIEW.md](docs/SECURITY_REVIEW.md)
144189
|--------|--------|
145190
| `webdev-backup.sh` | Main menu |
146191
| `backup.sh`, `restore.sh`, `quick-backup.sh` | Backup and restore |
192+
| `prune-backups.sh` | Prune old backups (keep 5 latest or delete one by one) |
147193
| `config.sh`, `utils.sh`, `fs.sh`, `ui.sh` | Config and shared modules |
148194
| `configure-cron.sh` | Cron schedules (uses mktemp) |
149195
| `compare-backups.sh` | Compare two backups |

archive/src.legacy/backup.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ fi
241241
# Get list of projects
242242
projects=()
243243
for dir in "${SOURCE_DIRS[@]}"; do
244-
mapfile -t dir_projects < <(find_projects "$dir" 1)
244+
dir_projects=()
245+
while IFS= read -r project_path; do
246+
[ -n "$project_path" ] && dir_projects+=("$project_path")
247+
done < <(find_projects "$dir" 1)
245248
projects+=("${dir_projects[@]}")
246249
done
247250

archive/src.legacy/restore.sh

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ echo -e "${CYAN}Started at: $(date)${NC}\n"
112112
if [ "$LIST_BACKUPS" = true ]; then
113113
echo -e "${YELLOW}Available Backups:${NC}"
114114

115-
# Find all backup directories
116-
mapfile -t backup_dirs < <(find "$BACKUP_DIR" -maxdepth 1 -type d -name "wsl2_backup_*" | sort -r)
115+
# Find all backup directories (Bash 3.2 compatible)
116+
backup_dirs=()
117+
while IFS= read -r backup_dir; do
118+
[ -n "$backup_dir" ] && backup_dirs+=("$backup_dir")
119+
done < <(find "$BACKUP_DIR" -maxdepth 1 -type d -name "wsl2_backup_*" | sort -r)
117120

118121
if [ ${#backup_dirs[@]} -eq 0 ]; then
119122
echo -e "${RED}No backups found in $BACKUP_DIR${NC}"
@@ -189,8 +192,11 @@ if [ "$DRY_RUN" = true ]; then
189192
log "Dry run mode: Showing what would be done, no actual restore" "$LOG_FILE"
190193
fi
191194

192-
# Find available projects in the backup
193-
mapfile -t available_projects < <(find "$BACKUP_TO_RESTORE" -maxdepth 1 -type f -name "*.tar.gz" | sed -n 's/.*\/\([^_]*\)_.*/\1/p' | sort -u)
195+
# Find available projects in the backup (Bash 3.2 compatible)
196+
available_projects=()
197+
while IFS= read -r project_name; do
198+
[ -n "$project_name" ] && available_projects+=("$project_name")
199+
done < <(find "$BACKUP_TO_RESTORE" -maxdepth 1 -type f -name "*.tar.gz" | sed -n 's/.*\/\([^_]*\)_.*/\1/p' | sort -u)
194200

195201
if [ ${#available_projects[@]} -eq 0 ]; then
196202
# Special handling for dry-run mode

archive/src.legacy/utils/dirs-status.sh

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ TOTAL_SIZE=0
2626
VALID_DIRS=0
2727
INVALID_DIRS=0
2828

29-
# Track project counts per directory
30-
declare -A PROJECT_COUNTS
29+
# Track project counts per directory (Bash 3.2 compatible; avoid associative arrays)
30+
PROJECT_DIR_NAMES=()
31+
PROJECT_DIR_COUNTS=()
3132

3233
# Process each directory
3334
for ((i=0; i<${#DEFAULT_SOURCE_DIRS[@]}; i++)); do
@@ -38,10 +39,14 @@ for ((i=0; i<${#DEFAULT_SOURCE_DIRS[@]}; i++)); do
3839

3940
# Check if directory exists and is readable
4041
if [ -d "$DIR" ] && [ -r "$DIR" ]; then
41-
# Find all projects (subdirs)
42-
readarray -t PROJECTS < <(find "$DIR" -maxdepth 1 -mindepth 1 -type d -not -path "*/\.*" | sort)
42+
# Find all projects (subdirs) - Bash 3.2 compatible
43+
PROJECTS=()
44+
while IFS= read -r project_path; do
45+
[ -n "$project_path" ] && PROJECTS+=("$project_path")
46+
done < <(find "$DIR" -maxdepth 1 -mindepth 1 -type d -not -path "*/\.*" | sort)
4347
PROJECT_COUNT=${#PROJECTS[@]}
44-
PROJECT_COUNTS["$DIR_NAME"]=$PROJECT_COUNT
48+
PROJECT_DIR_NAMES+=("$DIR_NAME")
49+
PROJECT_DIR_COUNTS+=("$PROJECT_COUNT")
4550

4651
TOTAL_PROJECTS=$((TOTAL_PROJECTS + PROJECT_COUNT))
4752
VALID_DIRS=$((VALID_DIRS + 1))
@@ -86,8 +91,8 @@ echo "Total size (excluding node_modules): $(format_size "$TOTAL_SIZE")"
8691

8792
echo
8893
echo -e "${CYAN}Projects by directory:${NC}"
89-
for DIR_NAME in "${!PROJECT_COUNTS[@]}"; do
90-
echo " $DIR_NAME: ${PROJECT_COUNTS[$DIR_NAME]} projects"
94+
for ((i=0; i<${#PROJECT_DIR_NAMES[@]}; i++)); do
95+
echo " ${PROJECT_DIR_NAMES[$i]}: ${PROJECT_DIR_COUNTS[$i]} projects"
9196
done
9297

9398
exit 0

archive/src.legacy/utils/error-handling.sh

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ readonly ERROR_WARNING=1 # Warning, non-critical
1616
readonly ERROR_CRITICAL=2 # Critical error, operation cannot continue
1717
readonly ERROR_FATAL=3 # Fatal error, script termination required
1818

19-
# Error codes and meanings
20-
declare -A ERROR_CODES=(
21-
[1]="Configuration error"
22-
[2]="File system error"
23-
[3]="Missing dependencies"
24-
[4]="Permission denied"
25-
[5]="Network error"
26-
[6]="Backup creation failed"
27-
[7]="Verification failed"
28-
[8]="Restore operation failed"
29-
[9]="Cloud upload/download failed"
30-
[10]="Invalid arguments"
31-
[99]="Unknown error"
32-
)
19+
# Error code lookup (Bash 3.2 compatible; avoid associative arrays)
20+
get_error_type() {
21+
case "${1:-99}" in
22+
1) echo "Configuration error" ;;
23+
2) echo "File system error" ;;
24+
3) echo "Missing dependencies" ;;
25+
4) echo "Permission denied" ;;
26+
5) echo "Network error" ;;
27+
6) echo "Backup creation failed" ;;
28+
7) echo "Verification failed" ;;
29+
8) echo "Restore operation failed" ;;
30+
9) echo "Cloud upload/download failed" ;;
31+
10) echo "Invalid arguments" ;;
32+
*) echo "Unknown error" ;;
33+
esac
34+
}
3335

3436
# Terminal colors
3537
RED='\033[0;31m'
@@ -52,7 +54,8 @@ handle_error() {
5254
local calling_line=$(caller | awk '{print $1}')
5355

5456
# Format error message for logging
55-
local error_type=${ERROR_CODES[$code]:-"Unknown error type"}
57+
local error_type
58+
error_type=$(get_error_type "$code")
5659
local log_message="[$timestamp] [${error_type}] (Code: $code) ${message}"
5760
local detail_message="In script $calling_script at line $calling_line"
5861

archive/src.legacy/utils/find-projects.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ for dir in "${SOURCE_DIRS[@]}"; do
4444
dir_name=$(basename "$dir")
4545
echo -e "\n${CYAN}=== Directory: $dir (${dir_name}) ===${NC}"
4646

47-
# Find projects in this directory
48-
mapfile -t projects < <(find "$dir" -maxdepth 1 -mindepth 1 -type d -not -path "*/\.*" | sort)
47+
# Find projects in this directory (Bash 3.2 compatible)
48+
projects=()
49+
while IFS= read -r project_path; do
50+
[ -n "$project_path" ] && projects+=("$project_path")
51+
done < <(find "$dir" -maxdepth 1 -mindepth 1 -type d -not -path "*/\.*" | sort)
4952

5053
# Display projects
5154
if [ ${#projects[@]} -gt 0 ]; then

backup.sh

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ CUSTOM_SOURCE_DIRS=()
2626
DRY_RUN=false
2727
EXTERNAL_BACKUP=false # Track if this is an external (cloud) backup
2828
QUICK_BACKUP=false # --quick: same as silent but show per-folder progress like interactive
29+
EXCLUDE_FILE="" # Temp file for exclusions; trap cleans on exit
30+
31+
trap 'rm -f "${EXCLUDE_FILE}" 2>/dev/null' EXIT
2932

3033
# Parse command line arguments
3134
while [[ $# -gt 0 ]]; do
@@ -120,8 +123,9 @@ while [[ $# -gt 0 ]]; do
120123
;;
121124
--destination|--dest|-d)
122125
if [[ -n "$2" && "$2" != --* ]]; then
123-
# Expand tilde in path
124-
CUSTOM_BACKUP_DIR="${2/#\~/$HOME}"
126+
# Expand tilde in path and validate (reject traversal, injection)
127+
_dir="${2/#\~/$HOME}"
128+
CUSTOM_BACKUP_DIR=$(validate_path "$_dir" "dir") || { echo -e "${RED}Error: Invalid destination path${NC}"; exit 1; }
125129
shift 2
126130
else
127131
echo -e "${RED}Error: Destination argument requires a directory path${NC}"
@@ -131,11 +135,12 @@ while [[ $# -gt 0 ]]; do
131135
--sources)
132136
if [[ -n "$2" ]]; then
133137
IFS=',' read -ra custom_dirs <<< "$2"
134-
for dir in "${custom_dirs[@]}"; do
135-
# Trim whitespace and expand tilde
136-
dir=$(echo "$dir" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
137-
dir="${dir/#\~/$HOME}"
138-
CUSTOM_SOURCE_DIRS+=("$dir")
138+
for _dir in "${custom_dirs[@]}"; do
139+
# Trim whitespace, expand tilde, and validate (reject traversal, injection)
140+
_dir=$(echo "$_dir" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
141+
_dir="${_dir/#\~/$HOME}"
142+
_validated=$(validate_path "$_dir" "dir") || { echo -e "${RED}Error: Invalid source path: $_dir${NC}"; exit 1; }
143+
CUSTOM_SOURCE_DIRS+=("$_validated")
139144
done
140145
shift 2
141146
else
@@ -145,9 +150,10 @@ while [[ $# -gt 0 ]]; do
145150
;;
146151
--source|-s)
147152
if [[ -n "$2" && "$2" != --* ]]; then
148-
# Expand tilde in path
149-
dir="${2/#\~/$HOME}"
150-
CUSTOM_SOURCE_DIRS+=("$dir")
153+
# Expand tilde in path and validate (reject traversal, injection)
154+
_dir="${2/#\~/$HOME}"
155+
_validated=$(validate_path "$_dir" "dir") || { echo -e "${RED}Error: Invalid source path${NC}"; exit 1; }
156+
CUSTOM_SOURCE_DIRS+=("$_validated")
151157
shift 2
152158
else
153159
echo -e "${RED}Error: Source argument requires a directory path${NC}"
@@ -867,8 +873,8 @@ if [ "$SILENT_MODE" = false ]; then
867873
echo -e "${YELLOW}=============================================${NC}"
868874
fi
869875

870-
# Generate HTML report (skip in dry-run)
871-
if [ "$DRY_RUN" != true ] && { [ "$SILENT_MODE" = false ] || [ -n "$EMAIL_NOTIFICATION" ]; }; then
876+
# Generate HTML report (skip in dry-run). Create when interactive, email requested, or when showing progress (e.g. --quick) so "view report" can be offered.
877+
if [ "$DRY_RUN" != true ] && { [ "$SILENT_MODE" = false ] || [ -n "$EMAIL_NOTIFICATION" ] || [ "$SHOW_PROGRESS" = true ]; }; then
872878
REPORT_FILE=$(create_backup_report \
873879
"$FULL_BACKUP_PATH" \
874880
"$SUCCESSFUL_PROJECTS" \
@@ -1039,10 +1045,10 @@ else
10391045
fi
10401046
echo -e "${CYAN}Finished at: $(date)${NC}\n"
10411047

1042-
# Ask if the user wants to view the report in browser
1043-
if [ "$DRY_RUN" != true ] && [ -f "$REPORT_FILE" ]; then
1048+
# Ask if the user wants to view the report in browser (default Y)
1049+
if [ "$DRY_RUN" != true ] && [ -n "$REPORT_FILE" ] && [ -f "$REPORT_FILE" ]; then
10441050
echo -e "\n${YELLOW}Would you like to view the backup report in your browser?${NC}"
1045-
if safe_confirm "Open report in browser?" "n"; then
1051+
if safe_confirm "Open report in browser?" "y"; then
10461052
echo -e "${GREEN}Opening backup report in browser...${NC}"
10471053
if open_in_browser "$REPORT_FILE"; then
10481054
echo -e "${GREEN}Report opened in browser.${NC}"

0 commit comments

Comments
 (0)