Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions examples/sample-output.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -133,4 +133,4 @@
"mcp_configs_count": 2,
"node_projects_count": 0
}
}
}
176 changes: 155 additions & 21 deletions stepsecurity-dev-machine-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
#==============================================================================
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -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+)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading