Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
Executable
+528
@@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
#### Salt State Linter ####
|
||||
#### ####
|
||||
#### Validates Salt .sls state files for common mistakes — duplicate ####
|
||||
#### state IDs, YAML syntax issues, missing requisites, Jinja errors, ####
|
||||
#### and anti-patterns. No dependencies beyond bash. ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./salt-state-linter.sh /srv/salt/ ####
|
||||
#### ./salt-state-linter.sh /srv/salt/nginx/init.sls ####
|
||||
#### ./salt-state-linter.sh --json /srv/salt/ ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version: 1.0 ####
|
||||
##############################################################################
|
||||
|
||||
set -o pipefail
|
||||
|
||||
readonly VERSION="1.0"
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# --- defaults ---
|
||||
OUTPUT_JSON=false
|
||||
QUIET=false
|
||||
USE_COLOR=true
|
||||
EXCLUDE_PATTERNS=()
|
||||
|
||||
# --- counters ---
|
||||
FILES_SCANNED=0
|
||||
TOTAL_ERRORS=0
|
||||
TOTAL_WARNINGS=0
|
||||
|
||||
# --- result collection ---
|
||||
declare -a RESULTS_TEXT=()
|
||||
declare -a RESULTS_JSON=()
|
||||
|
||||
# --- colors ---
|
||||
setup_colors() {
|
||||
if [[ "$USE_COLOR" == true ]] && [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
YLW='\033[0;33m'
|
||||
GRN='\033[0;32m'
|
||||
BLD='\033[1m'
|
||||
RST='\033[0m'
|
||||
else
|
||||
RED='' YLW='' GRN='' BLD='' RST=''
|
||||
fi
|
||||
}
|
||||
|
||||
# --- usage ---
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [OPTIONS] PATH [PATH...]
|
||||
|
||||
Validate Salt .sls state files for common mistakes.
|
||||
|
||||
Options:
|
||||
--json Output results as JSON
|
||||
--quiet Only show errors (no warnings)
|
||||
--no-color Disable colored output
|
||||
--exclude PATTERN Exclude files matching pattern (can repeat)
|
||||
-h, --help Show this help
|
||||
--version Show version
|
||||
|
||||
Exit codes:
|
||||
0 No errors found (warnings are OK)
|
||||
1 Errors found
|
||||
2 Usage error
|
||||
EOF
|
||||
}
|
||||
|
||||
# --- record finding ---
|
||||
record() {
|
||||
local file="$1" line="$2" level="$3" rule="$4" message="$5"
|
||||
|
||||
if [[ "$level" == "error" ]]; then
|
||||
((TOTAL_ERRORS++))
|
||||
local tag="ERROR"
|
||||
local color="$RED"
|
||||
else
|
||||
((TOTAL_WARNINGS++))
|
||||
if [[ "$QUIET" == true ]]; then
|
||||
return
|
||||
fi
|
||||
local tag="WARN"
|
||||
local color="$YLW"
|
||||
fi
|
||||
|
||||
RESULTS_TEXT+=("${color}${file}:${line}: ${tag}: ${message}${RST}")
|
||||
|
||||
# escape strings for JSON
|
||||
local jfile jmsg
|
||||
jfile="$(printf '%s' "$file" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
jmsg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
RESULTS_JSON+=("{\"file\":\"${jfile}\",\"line\":${line},\"level\":\"${level}\",\"rule\":\"${rule}\",\"message\":\"${jmsg}\"}")
|
||||
}
|
||||
|
||||
# --- check: duplicate state IDs ---
|
||||
check_duplicate_ids() {
|
||||
local file="$1"
|
||||
declare -A seen_ids=()
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
# state IDs: start at column 0, alphanumeric/underscore/hyphen, end with colon
|
||||
# skip comments, Jinja blocks, blank lines, lines starting with whitespace
|
||||
[[ "$line" =~ ^[[:space:]] ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
[[ "$line" =~ ^\{[%\{] ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == "---" ]] && continue
|
||||
[[ "$line" =~ ^include: ]] && continue
|
||||
|
||||
if [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_.-]*):(.*)$ ]]; then
|
||||
local id="${BASH_REMATCH[1]}"
|
||||
# skip salt module function lines (e.g., pkg.installed:)
|
||||
# state IDs don't contain dots typically; module.function does
|
||||
[[ "$id" == *.* ]] && continue
|
||||
|
||||
if [[ -n "${seen_ids[$id]+x}" ]]; then
|
||||
record "$file" "$lineno" "error" "duplicate-id" \
|
||||
"Duplicate state ID '${id}' (first seen line ${seen_ids[$id]})"
|
||||
else
|
||||
seen_ids["$id"]="$lineno"
|
||||
fi
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: tab indentation ---
|
||||
check_tabs() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
if [[ "$line" == *$'\t'* ]]; then
|
||||
record "$file" "$lineno" "error" "tab-indent" \
|
||||
"Tab character found — use spaces for YAML indentation"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: trailing whitespace ---
|
||||
check_trailing_whitespace() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
if [[ "$line" =~ [[:space:]]$ ]] && [[ -n "$line" ]]; then
|
||||
record "$file" "$lineno" "warning" "trailing-whitespace" \
|
||||
"Trailing whitespace"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: cmd.run without guards ---
|
||||
check_cmd_no_guard() {
|
||||
local file="$1"
|
||||
local -a lines=()
|
||||
local total=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
lines+=("$line")
|
||||
((total++))
|
||||
done < "$file"
|
||||
|
||||
local i=0
|
||||
while (( i < total )); do
|
||||
local line="${lines[$i]}"
|
||||
local lineno=$((i + 1))
|
||||
|
||||
# match cmd.run, cmd.script, cmd.wait in state function position (indented)
|
||||
if [[ "$line" =~ ^[[:space:]]+(cmd\.(run|script)):$ ]] || \
|
||||
[[ "$line" =~ ^[[:space:]]+-(\ )+cmd\.(run|script)$ ]]; then
|
||||
local has_guard=false
|
||||
local j=$((i + 1))
|
||||
# look ahead up to 15 lines for unless/onlyif/creates
|
||||
while (( j < total && j <= i + 15 )); do
|
||||
local next="${lines[$j]}"
|
||||
# stop if we hit another state ID or module function at same/lower indent
|
||||
[[ "$next" =~ ^[a-zA-Z] ]] && break
|
||||
if [[ "$next" =~ ^[[:space:]]+-(\ )+(unless|onlyif|creates): ]] || \
|
||||
[[ "$next" =~ ^[[:space:]]+(unless|onlyif|creates): ]]; then
|
||||
has_guard=true
|
||||
break
|
||||
fi
|
||||
((j++))
|
||||
done
|
||||
if [[ "$has_guard" == false ]]; then
|
||||
record "$file" "$lineno" "warning" "cmd-no-guard" \
|
||||
"cmd.run without unless/onlyif/creates (non-idempotent)"
|
||||
fi
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
}
|
||||
|
||||
# --- check: bare wildcard targeting ---
|
||||
check_bare_wildcard() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
if [[ "$line" =~ salt[[:space:]]+[\"\']\*[\"\'] ]] || \
|
||||
[[ "$line" =~ salt[[:space:]]+\'\*\' ]] || \
|
||||
[[ "$line" =~ \"tgt\":[[:space:]]*\"\*\" ]]; then
|
||||
record "$file" "$lineno" "warning" "bare-wildcard" \
|
||||
"Bare '*' targeting in SLS file — verify this is intentional"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: hardcoded secrets ---
|
||||
check_hardcoded_secrets() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
# skip comments and Jinja-only lines
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
|
||||
# check for password/secret patterns with inline values (not pillar refs)
|
||||
if [[ "$line" =~ (password|passwd|secret|api_key|private_key|token):[[:space:]]+[^\{] ]]; then
|
||||
local matched="${BASH_REMATCH[1]}"
|
||||
# skip if the value is a pillar/grains reference
|
||||
if [[ ! "$line" =~ (pillar|salt\[|grains) ]]; then
|
||||
record "$file" "$lineno" "warning" "hardcoded-secret" \
|
||||
"Possible hardcoded secret on line with '${matched}:' — use pillar data instead"
|
||||
fi
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: unclosed Jinja blocks ---
|
||||
check_jinja_syntax() {
|
||||
local file="$1"
|
||||
local content
|
||||
content="$(<"$file")"
|
||||
|
||||
# count opening and closing Jinja block tags
|
||||
local open_block close_block open_var close_var
|
||||
open_block=$(grep -o '{%' <<< "$content" 2>/dev/null | wc -l)
|
||||
close_block=$(grep -o '%}' <<< "$content" 2>/dev/null | wc -l)
|
||||
open_var=$(grep -o '{{' <<< "$content" 2>/dev/null | wc -l)
|
||||
close_var=$(grep -o '}}' <<< "$content" 2>/dev/null | wc -l)
|
||||
|
||||
if (( open_block != close_block )); then
|
||||
record "$file" 1 "error" "jinja-block" \
|
||||
"Mismatched Jinja block tags: ${open_block} '{%' vs ${close_block} '%}'"
|
||||
fi
|
||||
|
||||
if (( open_var != close_var )); then
|
||||
record "$file" 1 "error" "jinja-variable" \
|
||||
"Mismatched Jinja variable tags: ${open_var} '{{' vs ${close_var} '}}'"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- check: YAML boolean gotchas ---
|
||||
check_yaml_booleans() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
# skip comments
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
# skip Jinja lines
|
||||
[[ "$line" =~ ^\{[%\{] ]] && continue
|
||||
|
||||
# match key: value where value is bare yes/no/true/false (case-insensitive)
|
||||
# must be a YAML mapping value, not inside a Jinja expression
|
||||
if [[ "$line" =~ ^[[:space:]]+(-[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]+(yes|no|true|false|Yes|No|True|False|YES|NO|TRUE|FALSE)[[:space:]]*$ ]]; then
|
||||
local val="${BASH_REMATCH[2]}"
|
||||
# skip if the key itself is a known Salt boolean field
|
||||
if [[ "$line" =~ (enabled|enable|reload|watch_in|recurse|makedirs|replace|follow_symlinks|backup): ]]; then
|
||||
continue
|
||||
fi
|
||||
record "$file" "$lineno" "warning" "yaml-boolean" \
|
||||
"Bare YAML boolean '${val}' — quote it if a string value is intended"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: long lines ---
|
||||
check_long_lines() {
|
||||
local file="$1"
|
||||
local lineno=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
if (( ${#line} > 200 )); then
|
||||
record "$file" "$lineno" "warning" "long-line" \
|
||||
"Line exceeds 200 characters (${#line} chars)"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- check: file.managed without source or contents ---
|
||||
check_file_managed_no_source() {
|
||||
local file="$1"
|
||||
local -a lines=()
|
||||
local total=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
lines+=("$line")
|
||||
((total++))
|
||||
done < "$file"
|
||||
|
||||
local i=0
|
||||
while (( i < total )); do
|
||||
local line="${lines[$i]}"
|
||||
local lineno=$((i + 1))
|
||||
|
||||
if [[ "$line" =~ ^[[:space:]]+(file\.managed):$ ]] || \
|
||||
[[ "$line" =~ ^[[:space:]]+-(\ )+file\.managed$ ]]; then
|
||||
local has_source=false
|
||||
local j=$((i + 1))
|
||||
while (( j < total && j <= i + 20 )); do
|
||||
local next="${lines[$j]}"
|
||||
# stop at next state ID or module function
|
||||
[[ "$next" =~ ^[a-zA-Z] ]] && break
|
||||
if [[ "$next" =~ ^[[:space:]]+-(\ )+(source|contents|contents_pillar|contents_grains|contents_newline): ]] || \
|
||||
[[ "$next" =~ ^[[:space:]]+(source|contents|contents_pillar|contents_grains|contents_newline): ]]; then
|
||||
has_source=true
|
||||
break
|
||||
fi
|
||||
((j++))
|
||||
done
|
||||
if [[ "$has_source" == false ]]; then
|
||||
record "$file" "$lineno" "warning" "file-managed-no-source" \
|
||||
"file.managed without source or contents parameter"
|
||||
fi
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
}
|
||||
|
||||
# --- check: missing requisite targets ---
|
||||
check_missing_requisites() {
|
||||
local file="$1"
|
||||
|
||||
# first pass: collect all state IDs in the file
|
||||
declare -A state_ids=()
|
||||
local lineno=0
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
[[ "$line" =~ ^[[:space:]] ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
[[ "$line" =~ ^\{[%\{] ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == "---" ]] && continue
|
||||
[[ "$line" =~ ^include: ]] && continue
|
||||
|
||||
if [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_.-]*):(.*)$ ]]; then
|
||||
local id="${BASH_REMATCH[1]}"
|
||||
[[ "$id" == *.* ]] && continue
|
||||
state_ids["$id"]=1
|
||||
fi
|
||||
done < "$file"
|
||||
|
||||
# second pass: find requisite references
|
||||
lineno=0
|
||||
local in_requisite=false
|
||||
while IFS= read -r line; do
|
||||
((lineno++))
|
||||
|
||||
# match requisite target lines like: - pkg: nginx_install
|
||||
if [[ "$line" =~ ^[[:space:]]+-[[:space:]]+(pkg|file|service|cmd|user|group|git|pip|cron|mount|kmod):[[:space:]]+([a-zA-Z_][a-zA-Z0-9_.-]*)[[:space:]]*$ ]]; then
|
||||
local req_target="${BASH_REMATCH[2]}"
|
||||
# skip if target contains Jinja
|
||||
[[ "$req_target" =~ \{ ]] && continue
|
||||
if [[ -z "${state_ids[$req_target]+x}" ]]; then
|
||||
record "$file" "$lineno" "warning" "missing-requisite" \
|
||||
"Requisite target '${req_target}' not found as state ID in this file"
|
||||
fi
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# --- process a single file ---
|
||||
lint_file() {
|
||||
local file="$1"
|
||||
|
||||
# check exclude patterns
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ "$file" == *${pattern}* ]]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
((FILES_SCANNED++))
|
||||
|
||||
check_duplicate_ids "$file"
|
||||
check_tabs "$file"
|
||||
check_trailing_whitespace "$file"
|
||||
check_cmd_no_guard "$file"
|
||||
check_bare_wildcard "$file"
|
||||
check_hardcoded_secrets "$file"
|
||||
check_jinja_syntax "$file"
|
||||
check_yaml_booleans "$file"
|
||||
check_long_lines "$file"
|
||||
check_file_managed_no_source "$file"
|
||||
check_missing_requisites "$file"
|
||||
}
|
||||
|
||||
# --- collect .sls files ---
|
||||
collect_files() {
|
||||
local path="$1"
|
||||
if [[ -f "$path" ]]; then
|
||||
if [[ "$path" == *.sls ]]; then
|
||||
echo "$path"
|
||||
else
|
||||
echo "Warning: ${path} is not an .sls file, skipping" >&2
|
||||
fi
|
||||
elif [[ -d "$path" ]]; then
|
||||
find "$path" -type f -name '*.sls' | sort
|
||||
else
|
||||
echo "Error: ${path} does not exist" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- JSON output ---
|
||||
print_json() {
|
||||
echo "{"
|
||||
echo " \"files_scanned\": ${FILES_SCANNED},"
|
||||
echo " \"errors\": ${TOTAL_ERRORS},"
|
||||
echo " \"warnings\": ${TOTAL_WARNINGS},"
|
||||
echo " \"results\": ["
|
||||
|
||||
local count=${#RESULTS_JSON[@]}
|
||||
local i=0
|
||||
for entry in "${RESULTS_JSON[@]}"; do
|
||||
((i++))
|
||||
if (( i < count )); then
|
||||
echo " ${entry},"
|
||||
else
|
||||
echo " ${entry}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo " ]"
|
||||
echo "}"
|
||||
}
|
||||
|
||||
# --- text output ---
|
||||
print_text() {
|
||||
for line in "${RESULTS_TEXT[@]}"; do
|
||||
echo -e "$line"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${BLD}Scanned ${FILES_SCANNED} files${RST}"
|
||||
echo -e "Errors: ${RED}${TOTAL_ERRORS}${RST}"
|
||||
echo -e "Warnings: ${YLW}${TOTAL_WARNINGS}${RST}"
|
||||
}
|
||||
|
||||
# =============================================
|
||||
# Main
|
||||
# =============================================
|
||||
|
||||
main() {
|
||||
local paths=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--json) OUTPUT_JSON=true; shift ;;
|
||||
--quiet) QUIET=true; shift ;;
|
||||
--no-color) USE_COLOR=false; shift ;;
|
||||
--exclude)
|
||||
[[ -z "${2:-}" ]] && { echo "Error: --exclude requires a pattern" >&2; exit 2; }
|
||||
EXCLUDE_PATTERNS+=("$2"); shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--version) echo "$SCRIPT_NAME $VERSION"; exit 0 ;;
|
||||
-*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) paths+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#paths[@]} -eq 0 ]]; then
|
||||
echo "Error: no path specified" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
setup_colors
|
||||
|
||||
# collect and lint all files
|
||||
local all_files=()
|
||||
for p in "${paths[@]}"; do
|
||||
while IFS= read -r f; do
|
||||
all_files+=("$f")
|
||||
done < <(collect_files "$p")
|
||||
done
|
||||
|
||||
if [[ ${#all_files[@]} -eq 0 ]]; then
|
||||
echo "No .sls files found" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for f in "${all_files[@]}"; do
|
||||
lint_file "$f"
|
||||
done
|
||||
|
||||
# output results
|
||||
if [[ "$OUTPUT_JSON" == true ]]; then
|
||||
print_json
|
||||
else
|
||||
print_text
|
||||
fi
|
||||
|
||||
# exit code
|
||||
if (( TOTAL_ERRORS > 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user