This commit establishes the foundation for a comprehensive security audit tool for macOS developer machines. It audits all installed software, detects vulnerabilities via OSV.dev, and provides Claude-powered mitigation recommendations. ## Architecture - System inventory collection (Homebrew, pip, npm, Go, Rust, apps) - CVE scanning via OSV.dev API (no auth, unlimited rate limits) - Claude integration (CLI or SDK) for mitigation analysis - JSON + HTML report generation - Full test suite (34+ tests, 7 benchmarks) - Production-grade linting (11 linters via golangci-lint) - Vulnerability scanning (govulncheck) - GitHub Actions CI/CD pipeline ## Key Components - cmd/audit/: CLI entry point - internal/inventory/: System inventory parsing - internal/security/: OSV.dev querying, Claude integration, audit logic - internal/report/: JSON and HTML report generation - scripts/collect-inventory.sh: Bash script for system enumeration ## MVP Features - Homebrew package scanning - Python/Node/Go/Rust ecosystem scanning - CVSS-based severity filtering - Claude-powered recommendations (update/replace/protect/monitor) - Makefile automation (test, lint, vulnerability checks) - Pre-commit hooks configuration ## Testing & Quality - 34+ unit tests with table-driven patterns - 7 benchmark tests for performance - 11 configured linters (staticcheck, gosec, revive, etc.) - Code coverage reporting - GitHub Actions CI (tests on Go 1.22 & 1.23) - Pre-commit hook framework ## Documentation - README.md: Full feature and usage documentation - QUICKSTART.md: 3-minute setup guide - TESTING.md: Testing and code quality guide - QA_SETUP.md: Analysis tooling reference - DATA_SOURCES_SPEC.md: Vulnerability data source documentation ## Data Sources - OSV.dev: Primary CVE database (1.8 day latency, no auth needed) - GitHub Advisories: Supplement for maintainer-created advisories - OpenSSF Scorecard: Trust signals (repo health/practices) - Homebrew Formulae API: Package metadata - CISA KEV: Active exploitation tracking ## Next Steps Phase 1 (MVP): Complete and tested ✓ Phase 2 (planned): Third-party app scanning, VirusTotal integration Phase 3 (planned): Historical monitoring, scheduled audits, webhooks Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
176 lines
5.3 KiB
Bash
Executable File
176 lines
5.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Collect system inventory for security audit
|
|
# Outputs JSON with all installed packages and applications
|
|
|
|
set -e
|
|
|
|
OUTPUT_FILE="${1:-inventory.json}"
|
|
|
|
# Initialize JSON object
|
|
json_data="{"
|
|
json_data+="\"timestamp\":\"$(date -u +'%Y-%m-%dT%H:%M:%SZ')\","
|
|
json_data+="\"system\":{"
|
|
|
|
# System info
|
|
json_data+="\"os\":\"$(uname -s)\","
|
|
json_data+="\"kernel_release\":\"$(uname -r)\","
|
|
json_data+="\"macos_version\":\"$(sw_vers -productVersion)\","
|
|
json_data+="\"hostname\":\"$(hostname)\","
|
|
json_data+="\"username\":\"$(whoami)\""
|
|
json_data+="},"
|
|
|
|
# Homebrew packages
|
|
echo "Collecting Homebrew packages..." >&2
|
|
json_data+="\"homebrew\":["
|
|
if command -v brew &> /dev/null; then
|
|
first=true
|
|
while IFS= read -r line; do
|
|
if [ -z "$line" ]; then
|
|
continue
|
|
fi
|
|
name=$(echo "$line" | awk '{print $1}')
|
|
version=$(echo "$line" | awk '{print $2}')
|
|
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
json_data+=","
|
|
fi
|
|
|
|
json_data+="{\"name\":\"$(echo "$name" | sed 's/"/\\"/g')\",\"version\":\"$(echo "$version" | sed 's/"/\\"/g')\",\"source\":\"homebrew\"}"
|
|
done < <(brew list --versions 2>/dev/null || true)
|
|
fi
|
|
json_data+="],"
|
|
|
|
# Python packages (from system/brew Python)
|
|
echo "Collecting Python packages..." >&2
|
|
json_data+="\"python\":["
|
|
if command -v python3 &> /dev/null; then
|
|
first=true
|
|
while IFS= read -r line; do
|
|
if [ -z "$line" ] || [[ "$line" == "Package"* ]] || [[ "$line" == "---"* ]]; then
|
|
continue
|
|
fi
|
|
name=$(echo "$line" | awk '{print $1}')
|
|
version=$(echo "$line" | awk '{print $2}')
|
|
|
|
if [ -z "$name" ] || [ -z "$version" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
json_data+=","
|
|
fi
|
|
|
|
json_data+="{\"name\":\"$(echo "$name" | sed 's/"/\\"/g')\",\"version\":\"$(echo "$version" | sed 's/"/\\"/g')\",\"source\":\"pip\"}"
|
|
done < <(python3 -m pip list 2>/dev/null || true)
|
|
fi
|
|
json_data+="],"
|
|
|
|
# Node.js global packages
|
|
echo "Collecting Node.js global packages..." >&2
|
|
json_data+="\"node\":["
|
|
if command -v npm &> /dev/null; then
|
|
first=true
|
|
while IFS= read -r line; do
|
|
if [ -z "$line" ] || [[ "$line" == *"node_modules"* ]] || [[ "$line" == "├"* ]] || [[ "$line" == "└"* ]] || [[ "$line" == "│"* ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Parse package@version format
|
|
if [[ "$line" =~ ^([a-zA-Z0-9\-\./@]+)@([0-9\.]+) ]]; then
|
|
name="${BASH_REMATCH[1]}"
|
|
version="${BASH_REMATCH[2]}"
|
|
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
json_data+=","
|
|
fi
|
|
|
|
json_data+="{\"name\":\"$(echo "$name" | sed 's/"/\\"/g')\",\"version\":\"$(echo "$version" | sed 's/"/\\"/g')\",\"source\":\"npm\"}"
|
|
fi
|
|
done < <(npm list -g --depth=0 2>/dev/null || true)
|
|
fi
|
|
json_data+="],"
|
|
|
|
# Go packages (if go.mod exists in common locations)
|
|
echo "Collecting Go packages..." >&2
|
|
json_data+="\"go\":["
|
|
first=true
|
|
for go_mod in ~/go/src/*/go.mod ~/.config/*/go.mod /tmp/*/go.mod; do
|
|
if [ -f "$go_mod" ]; then
|
|
dir=$(dirname "$go_mod")
|
|
if command -v go &> /dev/null; then
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^require\ ]]; then
|
|
continue
|
|
fi
|
|
if [ -z "$line" ] || [[ "$line" == ")" ]] || [[ "$line" =~ ^\/\/ ]]; then
|
|
continue
|
|
fi
|
|
|
|
name=$(echo "$line" | awk '{print $1}')
|
|
version=$(echo "$line" | awk '{print $2}')
|
|
|
|
if [ -z "$name" ] || [ -z "$version" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
json_data+=","
|
|
fi
|
|
|
|
json_data+="{\"name\":\"$(echo "$name" | sed 's/"/\\"/g')\",\"version\":\"$(echo "$version" | sed 's/"/\\"/g')\",\"source\":\"go\"}"
|
|
done < <(go mod graph 2>/dev/null | cut -d' ' -f2 | sort -u || true)
|
|
fi
|
|
fi
|
|
done
|
|
json_data+="],"
|
|
|
|
# Installed applications
|
|
echo "Collecting installed applications..." >&2
|
|
json_data+="\"applications\":["
|
|
first=true
|
|
for app_path in /Applications/* /System/Applications/* /usr/local/opt/*/Applications/*; do
|
|
if [ -d "$app_path" ]; then
|
|
app_name=$(basename "$app_path" .app)
|
|
|
|
# Try to extract version from Info.plist
|
|
info_plist="$app_path/Contents/Info.plist"
|
|
version="unknown"
|
|
bundle_id=""
|
|
|
|
if [ -f "$info_plist" ]; then
|
|
# Extract CFBundleShortVersionString
|
|
if command -v plutil &> /dev/null; then
|
|
version=$(plutil -extract CFBundleShortVersionString raw "$info_plist" 2>/dev/null || echo "unknown")
|
|
bundle_id=$(plutil -extract CFBundleIdentifier raw "$info_plist" 2>/dev/null || echo "")
|
|
elif command -v defaults &> /dev/null; then
|
|
version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$info_plist" 2>/dev/null || echo "unknown")
|
|
bundle_id=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$info_plist" 2>/dev/null || echo "")
|
|
fi
|
|
fi
|
|
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
json_data+=","
|
|
fi
|
|
|
|
json_data+="{\"name\":\"$(echo "$app_name" | sed 's/"/\\"/g')\",\"path\":\"$(echo "$app_path" | sed 's/"/\\"/g')\",\"version\":\"$(echo "$version" | sed 's/"/\\"/g')\",\"bundle_id\":\"$(echo "$bundle_id" | sed 's/"/\\"/g')\",\"source\":\"application\"}"
|
|
fi
|
|
done
|
|
json_data+="]"
|
|
|
|
json_data+="}"
|
|
|
|
# Write to file
|
|
echo "$json_data" > "$OUTPUT_FILE"
|
|
echo "✓ Inventory saved to $OUTPUT_FILE" >&2
|
|
echo " Total applications: $(echo "$json_data" | grep -o '\"name\"' | wc -l)" >&2
|