mirror of
https://github.com/aljazceru/Auditor.git
synced 2025-12-17 03:24:18 +01:00
Fixed UX for deeps, wasnt reporting what packages it was updating, was just silent... Changed readme to remove the personal note.
212 lines
9.6 KiB
Python
212 lines
9.6 KiB
Python
"""Parse and analyze project dependencies."""
|
|
|
|
import platform
|
|
from pathlib import Path
|
|
import click
|
|
from theauditor.utils.error_handler import handle_exceptions
|
|
from theauditor.utils.exit_codes import ExitCodes
|
|
|
|
# Detect if running on Windows for character encoding
|
|
IS_WINDOWS = platform.system() == "Windows"
|
|
|
|
|
|
@click.command()
|
|
@handle_exceptions
|
|
@click.option("--root", default=".", help="Root directory")
|
|
@click.option("--check-latest", is_flag=True, help="Check for latest versions from registries")
|
|
@click.option("--upgrade-all", is_flag=True, help="YOLO mode: Update ALL packages to latest versions")
|
|
@click.option("--offline", is_flag=True, help="Force offline mode (no network)")
|
|
@click.option("--out", default="./.pf/raw/deps.json", help="Output dependencies file")
|
|
@click.option("--print-stats", is_flag=True, help="Print dependency statistics")
|
|
@click.option("--vuln-scan", is_flag=True, help="Scan dependencies for known vulnerabilities")
|
|
def deps(root, check_latest, upgrade_all, offline, out, print_stats, vuln_scan):
|
|
"""Parse and analyze project dependencies."""
|
|
from theauditor.deps import parse_dependencies, write_deps_json, check_latest_versions, write_deps_latest_json, upgrade_all_deps
|
|
from theauditor.vulnerability_scanner import scan_dependencies, write_vulnerabilities_json, format_vulnerability_report
|
|
import sys
|
|
|
|
# Parse dependencies
|
|
deps_list = parse_dependencies(root_path=root)
|
|
|
|
if not deps_list:
|
|
click.echo("No dependency files found (package.json, pyproject.toml, requirements.txt)")
|
|
click.echo(" Searched in: " + str(Path(root).resolve()))
|
|
return
|
|
|
|
write_deps_json(deps_list, output_path=out)
|
|
|
|
# Vulnerability scanning
|
|
if vuln_scan:
|
|
click.echo(f"\n[SCAN] Running native vulnerability scanners...")
|
|
click.echo(f" Using: npm audit, pip-audit (if available)")
|
|
|
|
vulnerabilities = scan_dependencies(deps_list, offline=offline)
|
|
|
|
if vulnerabilities:
|
|
# Write JSON report
|
|
vuln_output = out.replace("deps.json", "vulnerabilities.json")
|
|
write_vulnerabilities_json(vulnerabilities, output_path=vuln_output)
|
|
|
|
# Display human-readable report
|
|
report = format_vulnerability_report(vulnerabilities)
|
|
click.echo("\n" + report)
|
|
click.echo(f"\nDetailed report written to {vuln_output}")
|
|
|
|
# Exit with error code if critical vulnerabilities found
|
|
critical_count = sum(1 for v in vulnerabilities if v["severity"] == "critical")
|
|
if critical_count > 0:
|
|
click.echo(f"\n[FAIL] Found {critical_count} CRITICAL vulnerabilities - failing build")
|
|
sys.exit(ExitCodes.CRITICAL_SEVERITY)
|
|
else:
|
|
click.echo(f" [OK] No known vulnerabilities found in dependencies")
|
|
|
|
# Don't continue with other operations after vuln scan
|
|
return
|
|
|
|
# YOLO MODE: Upgrade all to latest
|
|
if upgrade_all and not offline:
|
|
click.echo("[YOLO MODE] Upgrading ALL packages to latest versions...")
|
|
click.echo(" [WARN] This may break things. That's the point!")
|
|
|
|
# Get latest versions
|
|
latest_info = check_latest_versions(deps_list, allow_net=True, offline=offline)
|
|
if not latest_info:
|
|
click.echo(" [FAIL] Failed to fetch latest versions")
|
|
return
|
|
|
|
# Check if all packages were successfully checked
|
|
failed_checks = sum(1 for info in latest_info.values() if info.get("error") is not None)
|
|
successful_checks = sum(1 for info in latest_info.values() if info.get("latest") is not None)
|
|
|
|
if failed_checks > 0:
|
|
click.echo(f"\n [WARN] Only {successful_checks}/{len(latest_info)} packages checked successfully")
|
|
click.echo(f" [FAIL] Cannot upgrade with {failed_checks} failed checks")
|
|
click.echo(" Fix network issues and try again")
|
|
return
|
|
|
|
# Upgrade all dependency files
|
|
upgraded = upgrade_all_deps(root_path=root, latest_info=latest_info, deps_list=deps_list)
|
|
|
|
# Count unique packages that were upgraded
|
|
unique_upgraded = len([1 for k, v in latest_info.items() if v.get("is_outdated", False)])
|
|
total_updated = sum(upgraded.values())
|
|
|
|
click.echo(f"\n[UPGRADED] Dependency files:")
|
|
for file_type, count in upgraded.items():
|
|
if count > 0:
|
|
click.echo(f" [OK] {file_type}: {count} dependency entries updated")
|
|
|
|
# Show what was actually upgraded
|
|
click.echo(f"\n[CHANGES] Packages upgraded:")
|
|
upgraded_packages = [
|
|
(k.split(":")[1], v["locked"], v["latest"], v.get("delta", ""))
|
|
for k, v in latest_info.items()
|
|
if v.get("is_outdated", False) and v.get("latest") is not None
|
|
]
|
|
|
|
# Sort by package name for consistent output
|
|
upgraded_packages.sort(key=lambda x: x[0].lower())
|
|
|
|
# Show first 20 upgrades with details
|
|
for name, old_ver, new_ver, delta in upgraded_packages[:20]:
|
|
delta_marker = " [MAJOR]" if delta == "major" else ""
|
|
# Use arrow character that works on Windows
|
|
arrow = "->" if IS_WINDOWS else "→"
|
|
click.echo(f" - {name}: {old_ver} {arrow} {new_ver}{delta_marker}")
|
|
|
|
if len(upgraded_packages) > 20:
|
|
click.echo(f" ... and {len(upgraded_packages) - 20} more packages")
|
|
|
|
# Show summary that matches the "Outdated: 10/29" format
|
|
if total_updated > unique_upgraded:
|
|
click.echo(f"\n Summary: {unique_upgraded} unique packages updated across {total_updated} occurrences")
|
|
|
|
click.echo("\n[NEXT STEPS]:")
|
|
click.echo(" 1. Run: pip install -r requirements.txt")
|
|
click.echo(" 2. Or: npm install")
|
|
click.echo(" 3. Pray it still works")
|
|
return
|
|
|
|
# Check latest versions if requested
|
|
latest_info = {}
|
|
if check_latest and not offline:
|
|
# Count unique packages first
|
|
unique_packages = {}
|
|
for dep in deps_list:
|
|
key = f"{dep['manager']}:{dep['name']}"
|
|
if key not in unique_packages:
|
|
unique_packages[key] = 0
|
|
unique_packages[key] += 1
|
|
|
|
click.echo(f"Checking {len(deps_list)} dependencies for updates...")
|
|
click.echo(f" Unique packages to check: {len(unique_packages)}")
|
|
click.echo(" Connecting to: npm registry and PyPI")
|
|
latest_info = check_latest_versions(deps_list, allow_net=True, offline=offline)
|
|
if latest_info:
|
|
write_deps_latest_json(latest_info, output_path=out.replace("deps.json", "deps_latest.json"))
|
|
|
|
# Count successful vs failed checks
|
|
successful_checks = sum(1 for info in latest_info.values() if info.get("latest") is not None)
|
|
failed_checks = sum(1 for info in latest_info.values() if info.get("error") is not None)
|
|
|
|
click.echo(f" [OK] Checked {successful_checks}/{len(unique_packages)} unique packages")
|
|
if failed_checks > 0:
|
|
click.echo(f" [WARN] {failed_checks} packages failed to check")
|
|
# Show first few errors
|
|
errors = [(k.split(":")[1], v["error"]) for k, v in latest_info.items() if v.get("error")][:3]
|
|
for pkg, err in errors:
|
|
click.echo(f" - {pkg}: {err}")
|
|
else:
|
|
click.echo(" [FAIL] Failed to check versions (network issue or offline mode)")
|
|
|
|
# Always show output
|
|
click.echo(f"Dependencies written to {out}")
|
|
|
|
# Count by manager
|
|
npm_count = sum(1 for d in deps_list if d["manager"] == "npm")
|
|
py_count = sum(1 for d in deps_list if d["manager"] == "py")
|
|
|
|
click.echo(f" Total: {len(deps_list)} dependencies")
|
|
if npm_count > 0:
|
|
click.echo(f" Node/npm: {npm_count}")
|
|
if py_count > 0:
|
|
click.echo(f" Python: {py_count}")
|
|
|
|
if latest_info:
|
|
# Count how many of the TOTAL deps are outdated (only if successfully checked)
|
|
outdated_deps = 0
|
|
checked_deps = 0
|
|
for dep in deps_list:
|
|
key = f"{dep['manager']}:{dep['name']}"
|
|
if key in latest_info and latest_info[key].get("latest") is not None:
|
|
checked_deps += 1
|
|
if latest_info[key]["is_outdated"]:
|
|
outdated_deps += 1
|
|
|
|
# Also count unique outdated packages
|
|
outdated_unique = sum(1 for info in latest_info.values() if info.get("is_outdated", False))
|
|
|
|
# Show outdated/checked rather than outdated/total
|
|
if checked_deps == len(deps_list):
|
|
# All were checked successfully
|
|
click.echo(f" Outdated: {outdated_deps}/{len(deps_list)}")
|
|
else:
|
|
# Some failed, show both numbers
|
|
click.echo(f" Outdated: {outdated_deps}/{checked_deps} checked ({len(deps_list)} total)")
|
|
|
|
# Show major updates
|
|
major_updates = [
|
|
(k.split(":")[1], v["locked"], v["latest"])
|
|
for k, v in latest_info.items()
|
|
if v.get("delta") == "major"
|
|
]
|
|
if major_updates:
|
|
click.echo("\n Major version updates available:")
|
|
for name, locked, latest in major_updates[:5]:
|
|
click.echo(f" - {name}: {locked} -> {latest}")
|
|
if len(major_updates) > 5:
|
|
click.echo(f" ... and {len(major_updates) - 5} more")
|
|
|
|
# Add a helpful hint if no network operation was performed
|
|
if not check_latest and not upgrade_all:
|
|
click.echo("\nTIP: Run with --check-latest to check for outdated packages.") |