diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d19f6..4ae7f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 See [VERSIONING.md](VERSIONING.md) for why the version starts at 1.8.1. +## [1.8.2] - 2026-03-17 + +### Added +- `--search-dirs DIR [DIR...]` flag to scan specific directories instead of `$HOME` (replaces default; repeatable) + - Accepts multiple directories in a single flag: `--search-dirs /tmp /opt /var` + - Supports repeated use: `--search-dirs /tmp --search-dirs /opt` + - Quoted paths with spaces work: `--search-dirs "/path/with spaces"` + ## [1.8.1] - 2026-03-10 First open-source release. The scanning engine was previously an internal enterprise tool (v1.0.0-v1.8.1) running in production. This release adds community mode for local-only scanning while keeping the enterprise codebase intact. @@ -36,4 +44,5 @@ First open-source release. The scanning engine was previously an internal enterp - Execution log capture and base64 encoding - Instance locking to prevent concurrent runs +[1.8.2]: https://github.com/step-security/dev-machine-guard/compare/v1.8.1...v1.8.2 [1.8.1]: https://github.com/step-security/dev-machine-guard/releases/tag/v1.8.1 diff --git a/examples/sample-output.json b/examples/sample-output.json index 51c1845..e605abe 100644 --- a/examples/sample-output.json +++ b/examples/sample-output.json @@ -1,5 +1,5 @@ { - "agent_version": "1.8.1", + "agent_version": "1.8.2", "scan_timestamp": 1741305600, "scan_timestamp_iso": "2026-03-07T00:00:00Z", "device": { @@ -133,4 +133,4 @@ "mcp_configs_count": 2, "node_projects_count": 0 } -} +} \ No newline at end of file diff --git a/stepsecurity-dev-machine-guard.sh b/stepsecurity-dev-machine-guard.sh index bb35c50..3e4aba6 100755 --- a/stepsecurity-dev-machine-guard.sh +++ b/stepsecurity-dev-machine-guard.sh @@ -27,6 +27,7 @@ # Options: # --enable-npm-scan Enable Node.js package scanning # --disable-npm-scan Disable Node.js package scanning +# --search-dirs DIR [DIR...] Search DIRs instead of $HOME (replaces default; repeatable) # --verbose Show progress messages # --color=WHEN auto | always | never (default: auto) # -v, --version Show version @@ -41,7 +42,7 @@ set -euo pipefail # SECTION 2: VERSION AND CLI DEFAULTS #============================================================================== -AGENT_VERSION="1.8.1" +AGENT_VERSION="1.8.2" # Output configuration (set by CLI flags) OUTPUT_FORMAT="pretty" # pretty | json | html @@ -50,6 +51,14 @@ COLOR_MODE="auto" # auto | always | never QUIET=true # Suppress progress messages by default in community mode ENABLE_NODE_PACKAGE_SCAN="auto" # auto | true | false +# Directories to search for projects and extensions (bash array) +# Default: user's home directory. Customize as needed, e.g.: +# SEARCH_DIRS=("\$HOME" "/Volumes/code") # home + encrypted partition +# SEARCH_DIRS=("/Volumes/code") # only encrypted partition +# SEARCH_DIRS=("\$HOME" "/Volumes/code" "/opt/work") # multiple locations +SEARCH_DIRS=("\$HOME") +_SEARCH_DIRS_SET=false + #============================================================================== # STEPSECURITY ENTERPRISE CONFIGURATION # Community users: leave these unchanged. They are only used in enterprise mode. @@ -524,6 +533,29 @@ get_user_directory() { return 0 } +resolve_search_directories() { + local user_home="$1" + local resolved_dirs=() + + for dir in "${SEARCH_DIRS[@]}"; do + # Resolve $HOME to the actual user home directory + local resolved="${dir/\$HOME/$user_home}" + if [ -d "$resolved" ]; then + resolved_dirs+=("$resolved") + else + print_progress "Warning: Search directory not found, skipping: $resolved" + fi + done + + if [ ${#resolved_dirs[@]} -eq 0 ]; then + print_progress "Warning: No valid search directories found, falling back to: $user_home" + echo "$user_home" + return + fi + + printf '%s\n' "${resolved_dirs[@]}" +} + #============================================================================== # LAUNCHD MANAGEMENT #============================================================================== @@ -3086,12 +3118,30 @@ run_scan() { fi step_done "Scanning MCP server configs" - # Collect extensions + # Resolve search directories + local search_dirs + search_dirs=$(resolve_search_directories "$user_home") + + # Collect extensions across all search directories step_start "Scanning IDE extensions" - local user_dir="$user_home" - local all_ext_result=$(collect_all_extensions "$user_dir") - local ide_extensions=$(echo "$all_ext_result" | head -1) - local ext_count=$(echo "$all_ext_result" | tail -1) + local ide_extensions="[]" + local ext_count=0 + while IFS= read -r search_dir; do + local dir_ext_result=$(collect_all_extensions "$search_dir") + local dir_extensions=$(echo "$dir_ext_result" | head -1) + local dir_ext_count=$(echo "$dir_ext_result" | tail -1) + # Merge JSON arrays + if [ "$dir_extensions" != "[]" ] && [ -n "$dir_extensions" ]; then + if [ "$ide_extensions" = "[]" ]; then + ide_extensions="$dir_extensions" + else + local existing_content=$(echo "$ide_extensions" | sed 's/^\[//;s/\]$//') + local new_content=$(echo "$dir_extensions" | sed 's/^\[//;s/\]$//') + ide_extensions="[${existing_content},${new_content}]" + fi + fi + ext_count=$((ext_count + dir_ext_count)) + done <<< "$search_dirs" step_done "Scanning IDE extensions" # Resolve ENABLE_NODE_PACKAGE_SCAN: in community mode, "auto" means "false" @@ -3119,10 +3169,36 @@ run_scan() { step_done "Scanning global packages" step_start "Scanning Node.js projects" - local node_scan_result=$(scan_node_projects "$user_dir" "$logged_in_user") - node_projects_file=$(echo "$node_scan_result" | sed -n '1p') - node_projects_count=$(echo "$node_scan_result" | sed -n '2p') - node_scan_duration=$(echo "$node_scan_result" | sed -n '3p') + # Scan across all search directories, merge results + local combined_projects_file=$(mktemp) + echo "[]" > "$combined_projects_file" + local total_node_projects_count=0 + local total_node_scan_duration=0 + while IFS= read -r search_dir; do + local node_scan_result=$(scan_node_projects "$search_dir" "$logged_in_user") + local dir_projects_file=$(echo "$node_scan_result" | sed -n '1p') + local dir_projects_count=$(echo "$node_scan_result" | sed -n '2p') + local dir_scan_duration=$(echo "$node_scan_result" | sed -n '3p') + total_node_projects_count=$((total_node_projects_count + dir_projects_count)) + total_node_scan_duration=$((total_node_scan_duration + dir_scan_duration)) + # Merge project files + if [ -n "$dir_projects_file" ] && [ -f "$dir_projects_file" ]; then + local existing=$(cat "$combined_projects_file") + if [ "$existing" = "[]" ]; then + cat "$dir_projects_file" > "$combined_projects_file" + else + local existing_content=$(echo "$existing" | sed 's/^\[//;s/\]$//') + local new_content=$(cat "$dir_projects_file" | sed 's/^\[//;s/\]$//') + if [ -n "$new_content" ]; then + echo "[${existing_content},${new_content}]" > "$combined_projects_file" + fi + fi + rm -f "$dir_projects_file" + fi + done <<< "$search_dirs" + node_projects_file="$combined_projects_file" + node_projects_count=$total_node_projects_count + node_scan_duration=$total_node_scan_duration step_done "Scanning Node.js projects" else step_skip "Node.js packages (use --enable-npm-scan)" @@ -3297,14 +3373,29 @@ EOF local ide_installations=$(detect_ide_installations "$logged_in_user") echo "" - # Get user directory to scan - local user_dir=$(get_user_directory) + # Resolve search directories + local search_dirs + search_dirs=$(resolve_search_directories "$user_home") echo "" - # Collect all IDE extensions - local all_ide_collection_result=$(collect_all_extensions "$user_dir") - local ide_extensions=$(echo "$all_ide_collection_result" | head -1) - local ide_extensions_count=$(echo "$all_ide_collection_result" | tail -1) + # Collect all IDE extensions across search directories + local ide_extensions="[]" + local ide_extensions_count=0 + while IFS= read -r search_dir; do + local dir_ext_result=$(collect_all_extensions "$search_dir") + local dir_extensions=$(echo "$dir_ext_result" | head -1) + local dir_ext_count=$(echo "$dir_ext_result" | tail -1) + if [ "$dir_extensions" != "[]" ] && [ -n "$dir_extensions" ]; then + if [ "$ide_extensions" = "[]" ]; then + ide_extensions="$dir_extensions" + else + local existing_content=$(echo "$ide_extensions" | sed 's/^\[//;s/\]$//') + local new_content=$(echo "$dir_extensions" | sed 's/^\[//;s/\]$//') + ide_extensions="[${existing_content},${new_content}]" + fi + fi + ide_extensions_count=$((ide_extensions_count + dir_ext_count)) + done <<< "$search_dirs" echo "" # AI Agent Detection (v1.6.0+) @@ -3375,11 +3466,35 @@ EOF node_global_packages_count=$(echo "$global_scan_result" | sed -n '2p') echo "" - # Scan for Node.js projects (returns file path) - local node_scan_result=$(scan_node_projects "$user_dir" "$logged_in_user") - node_projects_file=$(echo "$node_scan_result" | sed -n '1p') - node_projects_count=$(echo "$node_scan_result" | sed -n '2p') - node_scan_duration=$(echo "$node_scan_result" | sed -n '3p') + # Scan for Node.js projects across all search directories + local combined_projects_file=$(mktemp) + echo "[]" > "$combined_projects_file" + local total_node_projects_count=0 + local total_node_scan_duration=0 + while IFS= read -r search_dir; do + local node_scan_result=$(scan_node_projects "$search_dir" "$logged_in_user") + local dir_projects_file=$(echo "$node_scan_result" | sed -n '1p') + local dir_projects_count=$(echo "$node_scan_result" | sed -n '2p') + local dir_scan_duration=$(echo "$node_scan_result" | sed -n '3p') + total_node_projects_count=$((total_node_projects_count + dir_projects_count)) + total_node_scan_duration=$((total_node_scan_duration + dir_scan_duration)) + if [ -n "$dir_projects_file" ] && [ -f "$dir_projects_file" ]; then + local existing=$(cat "$combined_projects_file") + if [ "$existing" = "[]" ]; then + cat "$dir_projects_file" > "$combined_projects_file" + else + local existing_content=$(echo "$existing" | sed 's/^\[//;s/\]$//') + local new_content=$(cat "$dir_projects_file" | sed 's/^\[//;s/\]$//') + if [ -n "$new_content" ]; then + echo "[${existing_content},${new_content}]" > "$combined_projects_file" + fi + fi + rm -f "$dir_projects_file" + fi + done <<< "$search_dirs" + node_projects_file="$combined_projects_file" + node_projects_count=$total_node_projects_count + node_scan_duration=$total_node_scan_duration echo "" else @@ -3493,6 +3608,7 @@ Output formats (community mode, mutually exclusive): --html FILE HTML report saved to FILE Options: + --search-dirs DIR [DIR...] Search DIRs instead of \$HOME (replaces default; repeatable) --enable-npm-scan Enable Node.js package scanning --disable-npm-scan Disable Node.js package scanning --verbose Show progress messages (suppressed by default) @@ -3506,6 +3622,9 @@ Examples: $(basename "$0") --json > scan.json # JSON to file $(basename "$0") --html report.html # HTML report $(basename "$0") --verbose --enable-npm-scan # Verbose with npm scan + $(basename "$0") --search-dirs /Volumes/code # Search only /Volumes/code + $(basename "$0") --search-dirs /tmp /opt # Multiple dirs, one flag + $(basename "$0") --search-dirs "/path/with spaces" --search-dirs /opt # Mixed styles $(basename "$0") send-telemetry # Enterprise telemetry https://github.com/step-security/dev-machine-guard @@ -3561,6 +3680,21 @@ while [ $# -gt 0 ]; do fi shift ;; + --search-dirs) + shift + if [ $# -eq 0 ] || [[ "$1" == --* ]]; then + print_error "--search-dirs requires at least one directory path argument" + exit 1 + fi + if [ "$_SEARCH_DIRS_SET" = false ]; then + SEARCH_DIRS=() + _SEARCH_DIRS_SET=true + fi + while [ $# -gt 0 ] && [[ "$1" != --* ]]; do + SEARCH_DIRS+=("$1") + shift + done + ;; --verbose) QUIET=false shift