@objctp/opencode-shell-routines 1.2.0
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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/agents/shell-architect.md +88 -0
- package/agents/shell-expert.md +60 -0
- package/commands/shell-audit.md +47 -0
- package/commands/shell-batch-exec.md +48 -0
- package/commands/shell-new.md +57 -0
- package/commands/shell-routines-setup.md +66 -0
- package/commands/shell-test-run.md +46 -0
- package/opencode.json +19 -0
- package/package.json +34 -0
- package/plugins/shell-hooks.ts +150 -0
- package/scripts/lib-batch.sh +297 -0
- package/scripts/lib-common.sh +332 -0
- package/skills/shell-batch-operations/SKILL.md +97 -0
- package/skills/shell-batch-operations/assets/batch-template.sh +124 -0
- package/skills/shell-batch-operations/examples/data-pipeline.sh +157 -0
- package/skills/shell-batch-operations/examples/file-batch.sh +140 -0
- package/skills/shell-batch-operations/references/decision-tree.md +53 -0
- package/skills/shell-best-practices/SKILL.md +313 -0
- package/skills/shell-best-practices/assets/library.sh +142 -0
- package/skills/shell-best-practices/assets/minimal.sh +54 -0
- package/skills/shell-best-practices/assets/posix.sh +180 -0
- package/skills/shell-best-practices/assets/standard.sh +203 -0
- package/skills/shell-best-practices/references/patterns.md +386 -0
- package/skills/shell-best-practices/references/security.md +195 -0
- package/skills/shell-debugging/SKILL.md +115 -0
- package/skills/shell-debugging/examples/debug-session.md +165 -0
- package/skills/shell-debugging/references/debugging-guide.md +336 -0
- package/skills/shell-profiling/SKILL.md +154 -0
- package/skills/shell-profiling/examples/profile-session.md +225 -0
- package/skills/shell-profiling/references/optimisation-patterns.md +373 -0
- package/skills/shell-profiling/references/profiling-tools.md +318 -0
- package/skills/shell-profiling/scripts/bench.sh +82 -0
- package/skills/shell-profiling/scripts/trace-aggregate.sh +34 -0
- package/skills/shell-review/SKILL.md +61 -0
- package/skills/shell-review/examples/sample-review.md +42 -0
- package/skills/shell-review/references/guidelines.md +48 -0
- package/skills/shell-review/references/review-template.md +56 -0
- package/skills/shell-security/SKILL.md +128 -0
- package/skills/shell-security/examples/dangerous-command-review.md +231 -0
- package/skills/shell-security/examples/secure-script-example.sh +317 -0
- package/skills/shell-security/references/dangerous-commands.md +561 -0
- package/skills/shell-security/references/security-patterns.md +30 -0
- package/skills/shell-security/references/sensitive-files.md +525 -0
- package/skills/shell-security/scripts/security-audit.sh +208 -0
- package/skills/shell-test/SKILL.md +237 -0
- package/skills/shell-test/examples/test-example.md +74 -0
- package/skills/shell-test/references/advanced-patterns.md +52 -0
- package/skills/shell-test/references/assertions.md +184 -0
- package/skills/shell-test/references/test-template.md +60 -0
- package/skills/shell-test/scripts/public-coverage.sh +93 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shell-security
|
|
3
|
+
description: Audit a bash script for security risks the linters miss: destructive commands, system-file writes, hardcoded credentials, insecure permissions, and dynamic execution (eval/source). Run the bundled detector, classify each finding by severity, and offer safer fixes on approval. Use when checking a script's safety ("audit for vulnerabilities", "is this safe?", "secure this script"). For quoting and general standards use shell-best-practices; for overall quality review use shell-review.
|
|
4
|
+
allowed-tools: Read, Grep, Bash, Write, Edit
|
|
5
|
+
argument-hint: [script-path]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shell Security Skill
|
|
9
|
+
|
|
10
|
+
Analyse bash scripts for security vulnerabilities, warn about risks, and automatically fix issues with safer alternatives upon approval.
|
|
11
|
+
|
|
12
|
+
## Target
|
|
13
|
+
|
|
14
|
+
**Target script:** `$ARGUMENTS`
|
|
15
|
+
|
|
16
|
+
If `$ARGUMENTS` is not provided, prompt the user to specify which script to audit. If `$ARGUMENTS` is a directory, scan each `.sh` file in that directory.
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
1. **Read the script** -- Understand purpose and context
|
|
21
|
+
2. **Run `scripts/security-audit.sh "$TARGET"`** -- Do not replicate its grep patterns manually; always use the script
|
|
22
|
+
3. **Interpret results** -- Consult all `references/` files (`dangerous-commands.md`, `security-patterns.md`, `sensitive-files.md`) and `examples/` (`secure-script-example.sh` for safer patterns, `dangerous-command-review.md` for output format) for context; categorise as Fatal/Severe/Moderate. For dynamic execution findings (`eval`/`source` with variables), classify each as **by design** (developer tool inherently executes code), **needs review** (handles untrusted input), or **safe** (variable is internally generated). Report "by design" findings as informational, not actionable issues. See the Assessment guide in `references/dangerous-commands.md` under the Dynamic Execution section.
|
|
23
|
+
4. **Warn with context** -- Explain risk, provide line number, suggest safer alternative
|
|
24
|
+
5. **Offer to fix** -- For fixable issues, offer to apply safer alternatives
|
|
25
|
+
6. **Confirm before modifying** -- Always ask for approval before applying any fix
|
|
26
|
+
|
|
27
|
+
**Done when** every detector category has run via `scripts/security-audit.sh`; each finding has a line, severity (Fatal/Severe/Moderate), and safer alternative; every dynamic-execution finding is classified (by design / needs review / safe); and no fix is applied without explicit approval.
|
|
28
|
+
|
|
29
|
+
## Scope
|
|
30
|
+
|
|
31
|
+
This skill covers **destructive commands, credentials, sensitive files, insecure permissions, and dynamic execution patterns**. For quoting and general best practices, use shell-best-practices instead. For overall quality review, use shell-review instead.
|
|
32
|
+
|
|
33
|
+
## What It Checks (Unique Coverage)
|
|
34
|
+
|
|
35
|
+
| Category | Examples | Already Covered By |
|
|
36
|
+
| --------------------------- | ---------------------------------------------------- | ------------------ |
|
|
37
|
+
| **Destructive commands** | `rm -rf /`, `dd`, fork bombs, `chmod 777` | UNIQUE |
|
|
38
|
+
| **System file risks** | Editing `/etc/passwd`, `/etc/sudoers`, `/etc/shadow` | UNIQUE |
|
|
39
|
+
| **Credential exposure** | Hardcoded API keys, secrets in code | UNIQUE |
|
|
40
|
+
| **Sensitive file patterns** | `.env`, `.pem`, `.key`, credentials files | UNIQUE |
|
|
41
|
+
| **Dynamic execution** | `eval` with variables, dynamic `source`, indirect commands | UNIQUE |
|
|
42
|
+
|
|
43
|
+
**NOT covered here** (use existing tools):
|
|
44
|
+
| Issue | Use Instead |
|
|
45
|
+
|-------|-------------|
|
|
46
|
+
| Unquoted variables | ShellCheck, shell-best-practices |
|
|
47
|
+
| Pipe to `sh`/`bash` | shell-best-practices |
|
|
48
|
+
| Temp file issues | shell-best-practices |
|
|
49
|
+
| Syntax errors | bash -n hook |
|
|
50
|
+
| Formatting | shfmt hook |
|
|
51
|
+
|
|
52
|
+
## Auto-Fix Behaviour
|
|
53
|
+
|
|
54
|
+
**Auto-fixable issues** (applied upon approval):
|
|
55
|
+
|
|
56
|
+
- Add `--preserve-root` to rm commands
|
|
57
|
+
- Replace hardcoded credentials with environment variables
|
|
58
|
+
- Add confirmation prompts to dangerous commands
|
|
59
|
+
- Replace `chmod 777` with specific permissions
|
|
60
|
+
- Add guards before system file operations
|
|
61
|
+
|
|
62
|
+
**Requires manual review**:
|
|
63
|
+
|
|
64
|
+
- Fork bomb patterns (need architecture review)
|
|
65
|
+
- Destructive commands with variable paths
|
|
66
|
+
- System file modifications (needs intent clarification)
|
|
67
|
+
|
|
68
|
+
**Always confirm before modifying** -- even for auto-fixable issues, the workflow is:
|
|
69
|
+
|
|
70
|
+
1. Show the dangerous code
|
|
71
|
+
2. Explain the risk
|
|
72
|
+
3. Show the safer alternative
|
|
73
|
+
4. Prompt the user for approval before applying the fix
|
|
74
|
+
|
|
75
|
+
## Integration
|
|
76
|
+
|
|
77
|
+
**How shell-security fits with existing tooling:**
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ EXISTING TOOLING (runs first) │
|
|
82
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
83
|
+
│ Hooks (PostToolUse): ShellCheck │ shfmt │ bash -n │
|
|
84
|
+
│ LSP: bash-language-server (real-time) │
|
|
85
|
+
│ Skills: shell-best-practices (eval, quoting, temp) │
|
|
86
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
87
|
+
│
|
|
88
|
+
▼
|
|
89
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
90
|
+
│ shell-security (UNIQUE coverage only) │
|
|
91
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
92
|
+
│ Focus: Destructive commands │ System files │ Credentials │ 777 │
|
|
93
|
+
│ Action: Warn + Auto-fix (with permission) │
|
|
94
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
95
|
+
│
|
|
96
|
+
▼
|
|
97
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ shell-review (final output) │
|
|
99
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
100
|
+
│ Consolidates: ShellCheck + shell-security + other findings │
|
|
101
|
+
│ Output: Structured review with severity categories │
|
|
102
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Key point**: shell-security does NOT duplicate what ShellCheck/shell-best-practices already catch. It focuses on:
|
|
106
|
+
|
|
107
|
+
- **Destructive command severity** ([FATAL]/[SEVERE]/[MODERATE]) with security context
|
|
108
|
+
- **System file awareness** (knows `/etc/passwd` is sensitive)
|
|
109
|
+
- **Credential detection** (recognises API keys, secrets)
|
|
110
|
+
- **Dynamic execution detection** (flags eval/source with variable arguments)
|
|
111
|
+
- **Auto-fix capability** (can apply safer alternatives)
|
|
112
|
+
|
|
113
|
+
## References
|
|
114
|
+
|
|
115
|
+
- `references/dangerous-commands.md` -- Catalogue of destructive commands with risk levels and safer alternatives
|
|
116
|
+
- `references/security-patterns.md` -- Conceptual map of detection categories and auto-fix status
|
|
117
|
+
- `references/sensitive-files.md` -- Files that require careful handling
|
|
118
|
+
|
|
119
|
+
Always read all references and examples before producing audit results.
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
- `examples/dangerous-command-review.md` -- Sample review output with remediation
|
|
124
|
+
- `examples/secure-script-example.sh` -- Secure coding reference
|
|
125
|
+
|
|
126
|
+
## Scripts
|
|
127
|
+
|
|
128
|
+
- `scripts/security-audit.sh` -- Reusable audit script that runs all detection grep patterns against a target file or directory.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Dangerous Command Review Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates how the `shell-security` skill reviews a script containing dangerous commands and offers fixes.
|
|
4
|
+
|
|
5
|
+
## Input Script
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
#!/usr/bin/env bash
|
|
9
|
+
# deploy.sh - Deploy application to production
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
# Configuration
|
|
14
|
+
TARGET_DIR="/var/www/app"
|
|
15
|
+
BACKUP_DIR="/var/backups/app"
|
|
16
|
+
API_KEY="sk-1234567890abcdefghijklmnop"
|
|
17
|
+
DB_PASSWORD="SuperSecretPass123"
|
|
18
|
+
|
|
19
|
+
# Clean previous deployment
|
|
20
|
+
rm -rf /var/www/app_old
|
|
21
|
+
rm -rf $TARGET_DIR
|
|
22
|
+
|
|
23
|
+
# Create backup
|
|
24
|
+
tar -czf "$BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz" "$TARGET_DIR"
|
|
25
|
+
|
|
26
|
+
# Extract new files
|
|
27
|
+
tar -xzf deploy.tar.gz -C "$TARGET_DIR"
|
|
28
|
+
|
|
29
|
+
# Set permissions
|
|
30
|
+
chmod -R 777 "$TARGET_DIR"
|
|
31
|
+
|
|
32
|
+
# Update database
|
|
33
|
+
mysql -u root -p"$DB_PASSWORD" -e "UPDATE users SET active=1"
|
|
34
|
+
|
|
35
|
+
# Clean up temporary files
|
|
36
|
+
find / -name "*.tmp" -exec rm {} \;
|
|
37
|
+
|
|
38
|
+
# Restart service
|
|
39
|
+
systemctl restart app-service
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Review Output
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Scanning deploy.sh for security issues (unique coverage)...
|
|
46
|
+
|
|
47
|
+
Found 6 issues:
|
|
48
|
+
|
|
49
|
+
1. ◆ Line 13: rm -rf targeting system directory
|
|
50
|
+
What: rm -rf /var/www/app_old
|
|
51
|
+
Risk: Deletes directory without validation, could delete wrong path
|
|
52
|
+
Fix: Add validation and --preserve-root flag
|
|
53
|
+
[Apply fix] [Show diff] [Skip]
|
|
54
|
+
|
|
55
|
+
Proposed fix:
|
|
56
|
+
```bash
|
|
57
|
+
[[ -d "/var/www/app_old" ]] || { echo "Directory not found" >&2; exit 1; }
|
|
58
|
+
rm -rf --preserve-root /var/www/app_old
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
2. ● Line 14: Unquoted variable in rm command
|
|
62
|
+
What: rm -rf $TARGET_DIR
|
|
63
|
+
Risk: If TARGET_DIR is empty, may delete unexpected files
|
|
64
|
+
Fix: Quote variable and add --preserve-root
|
|
65
|
+
[Apply fix] [Show diff] [Skip]
|
|
66
|
+
|
|
67
|
+
Proposed fix:
|
|
68
|
+
```bash
|
|
69
|
+
rm -rf --preserve-root "$TARGET_DIR"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. ● Line 9: Hardcoded API key
|
|
73
|
+
What: API_KEY="sk-1234567890abcdefghijklmnop"
|
|
74
|
+
Risk: Credential exposed in version control
|
|
75
|
+
Fix: Use environment variable
|
|
76
|
+
[Apply fix] [Show diff] [Skip]
|
|
77
|
+
|
|
78
|
+
Proposed fix:
|
|
79
|
+
```bash
|
|
80
|
+
API_KEY="${API_KEY:?API_KEY environment variable not set}"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
4. ● Line 10: Hardcoded database password
|
|
84
|
+
What: DB_PASSWORD="SuperSecretPass123"
|
|
85
|
+
Risk: Credential exposed in version control
|
|
86
|
+
Fix: Use environment variable or credential store
|
|
87
|
+
[Apply fix] [Show diff] [Skip]
|
|
88
|
+
|
|
89
|
+
Proposed fix:
|
|
90
|
+
```bash
|
|
91
|
+
# Use MySQL config file instead
|
|
92
|
+
db_password="${DB_PASSWORD:?DB_PASSWORD not set}"
|
|
93
|
+
mysql -u root -p"$db_password" -e "..."
|
|
94
|
+
# OR: use ~/.my.cnf for credentials
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
5. ● Line 23: chmod 777 on application directory
|
|
98
|
+
What: chmod -R 777 "$TARGET_DIR"
|
|
99
|
+
Risk: Makes directory world-writable, security vulnerability
|
|
100
|
+
Fix: Use specific permissions (755 for dirs, 644 for files)
|
|
101
|
+
[Apply fix] [Show diff] [Skip]
|
|
102
|
+
|
|
103
|
+
Proposed fix:
|
|
104
|
+
```bash
|
|
105
|
+
find "$TARGET_DIR" -type d -exec chmod 755 {} \;
|
|
106
|
+
find "$TARGET_DIR" -type f -exec chmod 644 {} \;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
6. ▲ Line 29: find from root directory
|
|
110
|
+
What: find / -name "*.tmp" -exec rm {} \;
|
|
111
|
+
Risk: Searches entire filesystem, slow and risky
|
|
112
|
+
Fix: Limit search scope to specific directories
|
|
113
|
+
[Apply fix] [Show diff] [Skip]
|
|
114
|
+
|
|
115
|
+
Proposed fix:
|
|
116
|
+
```bash
|
|
117
|
+
find "$TARGET_DIR" -name "*.tmp" -delete
|
|
118
|
+
find /tmp -maxdepth 1 -name "*.tmp" -delete
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
Severity Summary:
|
|
123
|
+
◆ Fatal: 1
|
|
124
|
+
● Severe: 4
|
|
125
|
+
▲ Moderate: 1
|
|
126
|
+
|
|
127
|
+
Note: ShellCheck will catch other issues (unquoted variables on line 26, etc.)
|
|
128
|
+
Run shellcheck deploy.sh for additional linting.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Remediation Workflow
|
|
132
|
+
|
|
133
|
+
When the user selects `[Apply fix]` for all issues:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
✔ Applying fixes...
|
|
137
|
+
|
|
138
|
+
1. ✔ Added validation to rm -rf /var/www/app_old
|
|
139
|
+
2. ✔ Added quotes and --preserve-root to rm -rf $TARGET_DIR
|
|
140
|
+
3. ✔ Replaced hardcoded API_KEY with environment variable
|
|
141
|
+
4. ✔ Replaced hardcoded DB_PASSWORD with environment variable
|
|
142
|
+
5. ✔ Replaced chmod 777 with specific permissions
|
|
143
|
+
6. ✔ Limited find scope to TARGET_DIR
|
|
144
|
+
|
|
145
|
+
All fixes applied. Review the changes:
|
|
146
|
+
git diff deploy.sh
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Fixed Script
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
#!/usr/bin/env bash
|
|
153
|
+
# deploy.sh - Deploy application to production
|
|
154
|
+
|
|
155
|
+
set -euo pipefail
|
|
156
|
+
|
|
157
|
+
# Configuration - use environment variables for secrets
|
|
158
|
+
: "${TARGET_DIR:=/var/www/app}"
|
|
159
|
+
: "${BACKUP_DIR:=/var/backups/app}"
|
|
160
|
+
: "${API_KEY:?API_KEY environment variable not set}"
|
|
161
|
+
: "${DB_PASSWORD:?DB_PASSWORD environment variable not set}"
|
|
162
|
+
|
|
163
|
+
# Validate directories exist
|
|
164
|
+
for dir in "$TARGET_DIR" "$BACKUP_DIR"; do
|
|
165
|
+
[[ -d "$dir" ]] || { echo "Error: Directory not found: $dir" >&2; exit 1; }
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# Clean previous deployment (with validation)
|
|
169
|
+
if [[ -d "/var/www/app_old" ]]; then
|
|
170
|
+
rm -rf --preserve-root /var/www/app_old
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Create backup
|
|
174
|
+
tar -czf "$BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz" "$TARGET_DIR"
|
|
175
|
+
|
|
176
|
+
# Extract new files
|
|
177
|
+
tar -xzf deploy.tar.gz -C "$TARGET_DIR"
|
|
178
|
+
|
|
179
|
+
# Set permissions (specific, not 777)
|
|
180
|
+
find "$TARGET_DIR" -type d -exec chmod 755 {} \;
|
|
181
|
+
find "$TARGET_DIR" -type f -exec chmod 644 {} \;
|
|
182
|
+
|
|
183
|
+
# Update database (credentials from environment)
|
|
184
|
+
mysql -u root -p"$DB_PASSWORD" -e "UPDATE users SET active=1"
|
|
185
|
+
|
|
186
|
+
# Clean up temporary files (limited scope)
|
|
187
|
+
find "$TARGET_DIR" -name "*.tmp" -delete
|
|
188
|
+
find /tmp -maxdepth 1 -name "*.tmp" -delete
|
|
189
|
+
|
|
190
|
+
# Restart service
|
|
191
|
+
systemctl restart app-service
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Additional ShellCheck Findings
|
|
195
|
+
|
|
196
|
+
After shell-security fixes, ShellCheck may report:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
Line 35: mysql -u root -p"$DB_PASSWORD" -e "..."
|
|
200
|
+
^-- SC2154: DB_PASSWORD is referenced but not assigned.
|
|
201
|
+
(This is expected - we require it as environment variable)
|
|
202
|
+
|
|
203
|
+
Line 35: mysql -u root -p"$DB_PASSWORD" -e "..."
|
|
204
|
+
^-- SC2016: Expressions don't expand in single quotes.
|
|
205
|
+
(Use double quotes for variable expansion in command)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Warning Format Template
|
|
209
|
+
|
|
210
|
+
When the skill detects issues, it uses this format:
|
|
211
|
+
|
|
212
|
+
> Symbols are coloured in `security-audit.sh` output: ◆ purple (Fatal), ● red (Severe), ▲ yellow (Moderate), ✔ light green (OK).
|
|
213
|
+
|
|
214
|
+
```markdown
|
|
215
|
+
◆/●/▲ **[Severity] [Issue type detected]**
|
|
216
|
+
|
|
217
|
+
**What was detected:**
|
|
218
|
+
Line N: `code_snippet`
|
|
219
|
+
|
|
220
|
+
**Why this matters:**
|
|
221
|
+
- Risk explanation
|
|
222
|
+
- Impact assessment
|
|
223
|
+
- Common scenarios that cause problems
|
|
224
|
+
|
|
225
|
+
**Safer alternative:**
|
|
226
|
+
```bash
|
|
227
|
+
# Fixed code with explanation
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Shall I apply this fix? (This will [describe what the fix does])
|
|
231
|
+
```
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# secure-script-example.sh
|
|
3
|
+
# Demonstrates secure alternatives to common dangerous patterns
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
###
|
|
8
|
+
### :::: Configuration :::: ###########
|
|
9
|
+
###
|
|
10
|
+
|
|
11
|
+
# Require environment variables for secrets
|
|
12
|
+
: "${DATABASE_URL:?DATABASE_URL environment variable not set}"
|
|
13
|
+
: "${API_KEY:?API_KEY environment variable not set}"
|
|
14
|
+
: "${APP_HOME:?APP_HOME environment variable not set}"
|
|
15
|
+
|
|
16
|
+
# Directory paths with validation
|
|
17
|
+
BACKUP_DIR="${APP_HOME}/backups"
|
|
18
|
+
TEMP_DIR="${APP_HOME}/tmp"
|
|
19
|
+
LOG_DIR="${APP_HOME}/logs"
|
|
20
|
+
|
|
21
|
+
###
|
|
22
|
+
### :::: Initialization :::: ##########
|
|
23
|
+
###
|
|
24
|
+
|
|
25
|
+
# Create directories with secure permissions
|
|
26
|
+
function init_directories() {
|
|
27
|
+
local dir
|
|
28
|
+
for dir in "$BACKUP_DIR" "$TEMP_DIR" "$LOG_DIR"; do
|
|
29
|
+
if [[ ! -d "$dir" ]]; then
|
|
30
|
+
mkdir -p "$dir"
|
|
31
|
+
chmod 700 "$dir"
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
###
|
|
37
|
+
### :::: Safe File Operations :::: ####
|
|
38
|
+
###
|
|
39
|
+
|
|
40
|
+
# Safe file deletion with validation
|
|
41
|
+
function safe_delete() {
|
|
42
|
+
local target="$1"
|
|
43
|
+
local force="${2:-false}"
|
|
44
|
+
|
|
45
|
+
# Validate target exists
|
|
46
|
+
if [[ ! -e "$target" ]]; then
|
|
47
|
+
echo "Warning: Target does not exist: $target" >&2
|
|
48
|
+
return 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Validate target is within allowed directory
|
|
52
|
+
local real_target
|
|
53
|
+
real_target=$(realpath "$target")
|
|
54
|
+
if [[ ! "$real_target" =~ ^"$APP_HOME" ]]; then
|
|
55
|
+
echo "Error: Refusing to delete outside APP_HOME: $target" >&2
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Confirm deletion unless forced
|
|
60
|
+
if [[ "$force" != "true" ]]; then
|
|
61
|
+
echo "Delete: $target"
|
|
62
|
+
read -r -p "Confirm? [y/N] " response
|
|
63
|
+
[[ "$response" =~ ^[Yy]$ ]] || return 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Delete with safety flags
|
|
67
|
+
rm -rf --preserve-root -- "$target"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Safe temporary file creation
|
|
71
|
+
function safe_tempfile() {
|
|
72
|
+
local prefix="${1:-script}"
|
|
73
|
+
local tmpfile
|
|
74
|
+
|
|
75
|
+
tmpfile=$(mktemp "${TEMP_DIR}/${prefix}.XXXXXX") || return 1
|
|
76
|
+
chmod 600 "$tmpfile"
|
|
77
|
+
echo "$tmpfile"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Safe file writing with backup
|
|
81
|
+
function safe_write() {
|
|
82
|
+
local file="$1"
|
|
83
|
+
local content="$2"
|
|
84
|
+
|
|
85
|
+
# Validate file path
|
|
86
|
+
if [[ ! "$file" =~ ^"$APP_HOME" ]] && [[ ! "$file" =~ ^"$TEMP_DIR" ]]; then
|
|
87
|
+
echo "Error: Refusing to write outside APP_HOME: $file" >&2
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Create backup if file exists
|
|
92
|
+
if [[ -f "$file" ]]; then
|
|
93
|
+
local backup
|
|
94
|
+
backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
|
|
95
|
+
cp "$file" "$backup"
|
|
96
|
+
echo "Backup created: $backup"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Write content
|
|
100
|
+
printf '%s\n' "$content" >"$file"
|
|
101
|
+
chmod 600 "$file"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
###
|
|
105
|
+
### :::: Secure Permissions :::: ######
|
|
106
|
+
###
|
|
107
|
+
|
|
108
|
+
# Set specific permissions instead of 777
|
|
109
|
+
function set_permissions() {
|
|
110
|
+
local target="$1"
|
|
111
|
+
|
|
112
|
+
# Directories: 755 (rwxr-xr-x)
|
|
113
|
+
find "$target" -type d -exec chmod 755 {} \;
|
|
114
|
+
|
|
115
|
+
# Files: 644 (rw-r--r--)
|
|
116
|
+
find "$target" -type f -exec chmod 644 {} \;
|
|
117
|
+
|
|
118
|
+
# Executable scripts: 755
|
|
119
|
+
find "$target" -type f -name "*.sh" -exec chmod 755 {} \;
|
|
120
|
+
|
|
121
|
+
# Sensitive files: 600
|
|
122
|
+
find "$target" -type f \( -name "*.key" -o -name "*.pem" -o -name ".env" \) -exec chmod 600 {} \;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
###
|
|
126
|
+
### :::: Secure Database Operations :::: #####
|
|
127
|
+
###
|
|
128
|
+
|
|
129
|
+
# Use MySQL with config file instead of command-line password
|
|
130
|
+
function mysql_query() {
|
|
131
|
+
local query="$1"
|
|
132
|
+
|
|
133
|
+
# Create temporary MySQL config
|
|
134
|
+
local my_cnf
|
|
135
|
+
my_cnf=$(safe_tempfile mysql) || return 1
|
|
136
|
+
|
|
137
|
+
# Write credentials to temp config (will be deleted)
|
|
138
|
+
cat >"$my_cnf" <<EOF
|
|
139
|
+
[client]
|
|
140
|
+
user="${DB_USER:-root}"
|
|
141
|
+
password="${DB_PASSWORD}"
|
|
142
|
+
host="${DB_HOST:-localhost}"
|
|
143
|
+
EOF
|
|
144
|
+
|
|
145
|
+
# Execute query with config file
|
|
146
|
+
mysql --defaults-file="$my_cnf" -e "$query"
|
|
147
|
+
local status=$?
|
|
148
|
+
|
|
149
|
+
# Securely delete config
|
|
150
|
+
shred -u "$my_cnf"
|
|
151
|
+
|
|
152
|
+
return "$status"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
###
|
|
156
|
+
### :::: Safe Cleanup :::: ############
|
|
157
|
+
###
|
|
158
|
+
|
|
159
|
+
# Clean temporary files within scope only
|
|
160
|
+
function cleanup_temp_files() {
|
|
161
|
+
local days="${1:-7}"
|
|
162
|
+
|
|
163
|
+
# Only clean within TEMP_DIR
|
|
164
|
+
find "$TEMP_DIR" -type f -mtime +"$days" -delete
|
|
165
|
+
|
|
166
|
+
# Clean old backups (keep last 10)
|
|
167
|
+
find "$BACKUP_DIR" -maxdepth 1 -name 'backup-*.tar.gz' -printf '%T@ %p\0' |
|
|
168
|
+
sort -zrn |
|
|
169
|
+
tail -zn +11 |
|
|
170
|
+
cut -zd ' ' -f2- |
|
|
171
|
+
xargs -r0 rm --
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
###
|
|
175
|
+
### :::: Signal Handling :::: #########
|
|
176
|
+
###
|
|
177
|
+
|
|
178
|
+
# Secure trap handler (use function, not eval)
|
|
179
|
+
function cleanup_handler() {
|
|
180
|
+
local exit_code=$?
|
|
181
|
+
|
|
182
|
+
echo "Cleaning up..." >&2
|
|
183
|
+
|
|
184
|
+
# Remove temp files
|
|
185
|
+
[[ -d "${TEMP_DIR:-}" ]] && rm -rf -- "${TEMP_DIR:?}"/*
|
|
186
|
+
|
|
187
|
+
# Log exit
|
|
188
|
+
[[ -d "${LOG_DIR:-}" ]] && echo "Script exited with code: $exit_code" >>"$LOG_DIR/exit.log"
|
|
189
|
+
|
|
190
|
+
exit "$exit_code"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Register cleanup handler (function reference, not string)
|
|
194
|
+
trap cleanup_handler EXIT
|
|
195
|
+
|
|
196
|
+
###
|
|
197
|
+
### :::: Input Validation :::: ########
|
|
198
|
+
###
|
|
199
|
+
|
|
200
|
+
# Validate directory path
|
|
201
|
+
function validate_directory() {
|
|
202
|
+
local dir="$1"
|
|
203
|
+
local create="${2:-false}"
|
|
204
|
+
|
|
205
|
+
# Check if absolute path
|
|
206
|
+
[[ "$dir" = /* ]] || {
|
|
207
|
+
echo "Error: Path must be absolute: $dir" >&2
|
|
208
|
+
return 1
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Check if within allowed paths
|
|
212
|
+
if [[ ! "$dir" =~ ^"$APP_HOME" ]] && [[ ! "$dir" =~ ^/tmp ]]; then
|
|
213
|
+
echo "Error: Path outside allowed directories: $dir" >&2
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# Create if requested
|
|
218
|
+
if [[ "$create" == "true" ]] && [[ ! -d "$dir" ]]; then
|
|
219
|
+
mkdir -p "$dir"
|
|
220
|
+
chmod 755 "$dir"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
return 0
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Validate filename (prevent path traversal)
|
|
227
|
+
function validate_filename() {
|
|
228
|
+
local filename="$1"
|
|
229
|
+
|
|
230
|
+
# Reject path traversal attempts
|
|
231
|
+
if [[ "$filename" =~ \.\. ]]; then
|
|
232
|
+
echo "Error: Path traversal detected: $filename" >&2
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Reject absolute paths (should be relative)
|
|
237
|
+
if [[ "$filename" = /* ]]; then
|
|
238
|
+
echo "Error: Absolute path not allowed: $filename" >&2
|
|
239
|
+
return 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Reject special characters
|
|
243
|
+
if [[ "$filename" =~ [[:space:]] ]]; then
|
|
244
|
+
echo "Error: Spaces not allowed in filename: $filename" >&2
|
|
245
|
+
return 1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
return 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
###
|
|
252
|
+
### :::: Safe Command Execution :::: ##
|
|
253
|
+
###
|
|
254
|
+
|
|
255
|
+
# Execute command with timeout and resource limits
|
|
256
|
+
function safe_execute() {
|
|
257
|
+
local timeout="${1:-300}" # 5 minutes default
|
|
258
|
+
shift
|
|
259
|
+
local cmd=("$@")
|
|
260
|
+
|
|
261
|
+
# Set resource limits
|
|
262
|
+
ulimit -t "$timeout" # CPU time
|
|
263
|
+
ulimit -v 4194304 # Max 4GB virtual memory
|
|
264
|
+
ulimit -u 100 # Max 100 processes
|
|
265
|
+
|
|
266
|
+
# Execute with timeout
|
|
267
|
+
timeout "$timeout" "${cmd[@]}"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
###
|
|
271
|
+
### :::: Example Usage :::: ###########
|
|
272
|
+
###
|
|
273
|
+
|
|
274
|
+
function main() {
|
|
275
|
+
# Initialize
|
|
276
|
+
init_directories
|
|
277
|
+
|
|
278
|
+
# Example: Safe file operations
|
|
279
|
+
local tempfile
|
|
280
|
+
tempfile=$(safe_tempfile data)
|
|
281
|
+
echo "Processing data in $tempfile"
|
|
282
|
+
|
|
283
|
+
# Example: Safe deletion
|
|
284
|
+
# safe_delete "$APP_HOME/old_data" false # Ask for confirmation
|
|
285
|
+
# safe_delete "$APP_HOME/cache" true # Force deletion
|
|
286
|
+
|
|
287
|
+
# Example: Set secure permissions
|
|
288
|
+
# set_permissions "$APP_HOME/public"
|
|
289
|
+
|
|
290
|
+
# Example: Cleanup
|
|
291
|
+
cleanup_temp_files 30
|
|
292
|
+
|
|
293
|
+
echo "Operations completed successfully"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Run main if executed directly
|
|
297
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
298
|
+
main "$@"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
###
|
|
302
|
+
### :::: Security Checklist :::: ###########
|
|
303
|
+
###
|
|
304
|
+
|
|
305
|
+
# ✔ set -euo pipefail for error handling
|
|
306
|
+
# ✔ Environment variables for secrets (no hardcoding)
|
|
307
|
+
# ✔ Path validation before operations
|
|
308
|
+
# ✔ realpath for canonical paths
|
|
309
|
+
# ✔ --preserve-root for rm
|
|
310
|
+
# ✔ mktemp for temporary files
|
|
311
|
+
# ✔ Specific permissions (no 777)
|
|
312
|
+
# ✔ Function references in trap (not eval)
|
|
313
|
+
# ✔ Input validation (path traversal prevention)
|
|
314
|
+
# ✔ Resource limits for commands
|
|
315
|
+
# ✔ Secure credential handling (MySQL config file)
|
|
316
|
+
# ✔ Cleanup handlers for temp files
|
|
317
|
+
# ✔ Confirmation prompts for destructive operations
|