408 lines
12 KiB
Bash
Executable File
408 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# ============================================================================
|
|
# Permission Checker - Audit file permissions for security issues
|
|
# ============================================================================
|
|
# Purpose: Detect world-writable files, overly permissive scripts, and permission issues
|
|
# Version: 1.0.0
|
|
# Usage: ./permission-checker.sh <path> <strict> <check_executables> <report_all>
|
|
# Returns: 0=all permissions correct, 1=issues found, 2=error
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
# Source shared validation library
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
|
|
|
if [[ -f "${PLUGIN_ROOT}/scripts/validate-lib.sh" ]]; then
|
|
source "${PLUGIN_ROOT}/scripts/validate-lib.sh"
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
PATH_TO_SCAN="${1:-.}"
|
|
STRICT="${2:-false}"
|
|
CHECK_EXECUTABLES="${3:-true}"
|
|
REPORT_ALL="${4:-false}"
|
|
|
|
ISSUES_FOUND=0
|
|
declare -a FINDINGS=()
|
|
|
|
# ============================================================================
|
|
# Permission Classification
|
|
# ============================================================================
|
|
|
|
get_permission_octal() {
|
|
local file="$1"
|
|
stat -f "%Op" "${file}" 2>/dev/null | sed 's/.*\([0-7][0-7][0-7][0-7]\)$/\1/' || \
|
|
stat -c "%a" "${file}" 2>/dev/null || echo "0644"
|
|
}
|
|
|
|
get_permission_symbolic() {
|
|
local file="$1"
|
|
ls -ld "${file}" 2>/dev/null | awk '{print $1}' | tail -c 10
|
|
}
|
|
|
|
is_world_writable() {
|
|
local perms="$1"
|
|
[[ "${perms: -1}" =~ [2367] ]]
|
|
}
|
|
|
|
is_world_readable() {
|
|
local perms="$1"
|
|
[[ "${perms: -1}" =~ [4567] ]]
|
|
}
|
|
|
|
is_executable() {
|
|
local perms="$1"
|
|
[[ "${perms}" =~ [1357] ]]
|
|
}
|
|
|
|
# ============================================================================
|
|
# Severity Classification
|
|
# ============================================================================
|
|
|
|
get_issue_severity() {
|
|
local issue_type="$1"
|
|
local perms="$2"
|
|
|
|
case "${issue_type}" in
|
|
world_writable_executable)
|
|
echo "critical"
|
|
;;
|
|
world_writable)
|
|
echo "critical"
|
|
;;
|
|
missing_shebang)
|
|
echo "high"
|
|
;;
|
|
overly_permissive_sensitive)
|
|
echo "high"
|
|
;;
|
|
wrong_directory_perms)
|
|
echo "medium"
|
|
;;
|
|
non_executable_script)
|
|
echo "medium"
|
|
;;
|
|
inconsistent_perms)
|
|
echo "low"
|
|
;;
|
|
*)
|
|
echo "low"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Shebang Validation
|
|
# ============================================================================
|
|
|
|
has_shebang() {
|
|
local file="$1"
|
|
|
|
if [[ ! -f "${file}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
local first_line
|
|
first_line=$(head -n 1 "${file}" 2>/dev/null || echo "")
|
|
|
|
[[ "${first_line}" =~ ^#! ]]
|
|
}
|
|
|
|
get_expected_shebang() {
|
|
local file="$1"
|
|
local basename
|
|
basename=$(basename "${file}")
|
|
|
|
case "${basename}" in
|
|
*.sh|*.bash)
|
|
echo "#!/usr/bin/env bash"
|
|
;;
|
|
*.py)
|
|
echo "#!/usr/bin/env python3"
|
|
;;
|
|
*.js)
|
|
echo "#!/usr/bin/env node"
|
|
;;
|
|
*.rb)
|
|
echo "#!/usr/bin/env ruby"
|
|
;;
|
|
*)
|
|
echo ""
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Expected Permissions
|
|
# ============================================================================
|
|
|
|
get_expected_permissions() {
|
|
local file="$1"
|
|
local basename
|
|
basename=$(basename "${file}")
|
|
local is_exec
|
|
|
|
# Check if currently executable
|
|
if [[ -x "${file}" ]]; then
|
|
is_exec="true"
|
|
else
|
|
is_exec="false"
|
|
fi
|
|
|
|
# Sensitive files
|
|
if [[ "${basename}" =~ ^\.env || "${basename}" =~ credentials || "${basename}" =~ secrets ]]; then
|
|
echo "600"
|
|
return
|
|
fi
|
|
|
|
# SSH/GPG files
|
|
if [[ "${file}" =~ \.ssh/id_ || "${file}" =~ \.gnupg/ ]]; then
|
|
if [[ "${basename}" =~ \.pub$ ]]; then
|
|
echo "644"
|
|
else
|
|
echo "600"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
# Scripts
|
|
if [[ "${basename}" =~ \.(sh|bash|py|js|rb)$ ]]; then
|
|
if [[ "${is_exec}" == "true" ]] || has_shebang "${file}"; then
|
|
echo "755"
|
|
else
|
|
echo "644"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
# Directories
|
|
if [[ -d "${file}" ]]; then
|
|
if [[ "${basename}" =~ ^\.ssh$ || "${basename}" =~ ^\.gnupg$ ]]; then
|
|
echo "700"
|
|
else
|
|
echo "755"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
# Default
|
|
echo "644"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Permission Checking
|
|
# ============================================================================
|
|
|
|
check_file_permissions() {
|
|
local file="$1"
|
|
local perms
|
|
perms=$(get_permission_octal "${file}")
|
|
local symbolic
|
|
symbolic=$(get_permission_symbolic "${file}")
|
|
local expected
|
|
expected=$(get_expected_permissions "${file}")
|
|
local basename
|
|
basename=$(basename "${file}")
|
|
|
|
# Skip certain directories
|
|
if [[ "${file}" =~ (\.git|node_modules|vendor|dist|build)/ ]]; then
|
|
return
|
|
fi
|
|
|
|
# CRITICAL: Check for 777 (world-writable and executable)
|
|
if [[ "${perms}" == "0777" || "${perms}" == "777" ]]; then
|
|
local issue_type="world_writable_executable"
|
|
local severity
|
|
severity=$(get_issue_severity "${issue_type}" "${perms}")
|
|
FINDINGS+=("${severity}|${file}|${perms}|${symbolic}|${expected}|World-writable and executable|Anyone can modify and execute|chmod ${expected} \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
return
|
|
fi
|
|
|
|
# CRITICAL: Check for 666 (world-writable)
|
|
if [[ "${perms}" == "0666" || "${perms}" == "666" ]]; then
|
|
local issue_type="world_writable"
|
|
local severity
|
|
severity=$(get_issue_severity "${issue_type}" "${perms}")
|
|
FINDINGS+=("${severity}|${file}|${perms}|${symbolic}|${expected}|World-writable file|Anyone can modify content|chmod ${expected} \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
return
|
|
fi
|
|
|
|
# Check if executable but missing shebang
|
|
if [[ -f "${file}" && -x "${file}" && "${CHECK_EXECUTABLES}" == "true" ]]; then
|
|
if [[ "${basename}" =~ \.(sh|bash|py|js|rb)$ ]]; then
|
|
if ! has_shebang "${file}"; then
|
|
local expected_shebang
|
|
expected_shebang=$(get_expected_shebang "${file}")
|
|
FINDINGS+=("high|${file}|${perms}|${symbolic}|${perms}|Executable without shebang|May not execute correctly|Add ${expected_shebang} to first line")
|
|
((ISSUES_FOUND++))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check sensitive files
|
|
if [[ "${basename}" =~ ^\.env || "${basename}" =~ credentials || "${basename}" =~ secrets ]]; then
|
|
if is_world_readable "${perms}"; then
|
|
FINDINGS+=("high|${file}|${perms}|${symbolic}|600|Sensitive file world-readable|Secrets visible to all users|chmod 600 \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
return
|
|
fi
|
|
if [[ "${perms}" != "0600" && "${perms}" != "600" && "${STRICT}" == "true" ]]; then
|
|
FINDINGS+=("medium|${file}|${perms}|${symbolic}|600|Sensitive file should be 600|Reduce permissions|chmod 600 \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# Strict mode: Check for any discrepancies
|
|
if [[ "${STRICT}" == "true" ]]; then
|
|
if [[ "${perms}" != "0${expected}" && "${perms}" != "${expected}" ]]; then
|
|
# Check if it's a minor discrepancy
|
|
if [[ "${perms}" =~ ^0?775$ && "${expected}" == "755" ]]; then
|
|
FINDINGS+=("medium|${file}|${perms}|${symbolic}|${expected}|Group-writable (strict mode)|Remove group write|chmod ${expected} \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
elif [[ "${perms}" =~ ^0?755$ && "${expected}" == "644" ]]; then
|
|
FINDINGS+=("low|${file}|${perms}|${symbolic}|${expected}|Executable but should not be|Remove executable bit|chmod ${expected} \"${file}\"")
|
|
((ISSUES_FOUND++))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Report all mode
|
|
if [[ "${REPORT_ALL}" == "true" ]]; then
|
|
if [[ "${perms}" == "0${expected}" || "${perms}" == "${expected}" ]]; then
|
|
FINDINGS+=("info|${file}|${perms}|${symbolic}|${expected}|Permissions correct|N/A|N/A")
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Execution
|
|
# ============================================================================
|
|
|
|
main() {
|
|
# Validate path
|
|
if [[ ! -e "${PATH_TO_SCAN}" ]]; then
|
|
echo "ERROR: Path does not exist: ${PATH_TO_SCAN}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
echo "File Permission Audit Results"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "Path: ${PATH_TO_SCAN}"
|
|
echo "Strict Mode: ${STRICT}"
|
|
echo "Check Executables: ${CHECK_EXECUTABLES}"
|
|
echo ""
|
|
|
|
# Scan files
|
|
local files_checked=0
|
|
|
|
if [[ -f "${PATH_TO_SCAN}" ]]; then
|
|
check_file_permissions "${PATH_TO_SCAN}"
|
|
((files_checked++))
|
|
elif [[ -d "${PATH_TO_SCAN}" ]]; then
|
|
while IFS= read -r -d '' file; do
|
|
check_file_permissions "${file}"
|
|
((files_checked++))
|
|
done < <(find "${PATH_TO_SCAN}" -print0 2>/dev/null)
|
|
fi
|
|
|
|
echo "Files Checked: ${files_checked}"
|
|
echo ""
|
|
|
|
# Report findings
|
|
if [[ ${ISSUES_FOUND} -eq 0 ]]; then
|
|
echo "✅ SUCCESS: All file permissions correct"
|
|
echo "No permission issues detected"
|
|
exit 0
|
|
fi
|
|
|
|
echo "⚠️ PERMISSION ISSUES DETECTED: ${ISSUES_FOUND}"
|
|
echo ""
|
|
|
|
# Group by severity
|
|
local critical_count=0
|
|
local high_count=0
|
|
local medium_count=0
|
|
local low_count=0
|
|
|
|
for finding in "${FINDINGS[@]}"; do
|
|
IFS='|' read -r severity file perms symbolic expected issue risk fix <<< "${finding}"
|
|
case "${severity}" in
|
|
critical) ((critical_count++)) ;;
|
|
high) ((high_count++)) ;;
|
|
medium) ((medium_count++)) ;;
|
|
low) ((low_count++)) ;;
|
|
info) ;; # Don't count info
|
|
esac
|
|
done
|
|
|
|
# Print findings by severity
|
|
if [[ ${critical_count} -gt 0 ]]; then
|
|
echo "CRITICAL Issues (${critical_count}):"
|
|
for finding in "${FINDINGS[@]}"; do
|
|
IFS='|' read -r severity file perms symbolic expected issue risk fix <<< "${finding}"
|
|
if [[ "${severity}" == "critical" ]]; then
|
|
echo " ❌ ${file} (${perms})"
|
|
echo " Current: ${symbolic} (${perms})"
|
|
echo " Issue: ${issue}"
|
|
echo " Risk: ${risk}"
|
|
echo " Fix: ${fix}"
|
|
echo ""
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ ${high_count} -gt 0 ]]; then
|
|
echo "HIGH Issues (${high_count}):"
|
|
for finding in "${FINDINGS[@]}"; do
|
|
IFS='|' read -r severity file perms symbolic expected issue risk fix <<< "${finding}"
|
|
if [[ "${severity}" == "high" ]]; then
|
|
echo " ⚠️ ${file} (${perms})"
|
|
echo " Issue: ${issue}"
|
|
echo " Fix: ${fix}"
|
|
echo ""
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ ${medium_count} -gt 0 ]]; then
|
|
echo "MEDIUM Issues (${medium_count}):"
|
|
for finding in "${FINDINGS[@]}"; do
|
|
IFS='|' read -r severity file perms symbolic expected issue risk fix <<< "${finding}"
|
|
if [[ "${severity}" == "medium" ]]; then
|
|
echo " 💡 ${file} (${perms})"
|
|
echo " Recommendation: ${issue}"
|
|
echo " Fix: ${fix}"
|
|
echo ""
|
|
fi
|
|
done
|
|
fi
|
|
|
|
echo "Summary:"
|
|
echo " Critical: ${critical_count}"
|
|
echo " High: ${high_count}"
|
|
echo " Medium: ${medium_count}"
|
|
echo " Low: ${low_count}"
|
|
echo ""
|
|
|
|
if [[ ${critical_count} -gt 0 ]]; then
|
|
echo "Action Required: FIX IMMEDIATELY"
|
|
elif [[ ${high_count} -gt 0 ]]; then
|
|
echo "Action Required: YES"
|
|
else
|
|
echo "Action Required: REVIEW"
|
|
fi
|
|
|
|
exit 1
|
|
}
|
|
|
|
main "$@"
|