Skill: phy041/phy-lock-file-auditor (version 1.0.3)

--- name: Lock File Auditor description: Lockfile security and integrity auditor for JavaScript, Python, Go, and Rust projects. Detects phantom dependencies (packages in lockfile but not in manifest), lockfile drift (manifest changed without re-running install), integrity hash anomalies (missing, malformed, or duplicate SHA-512/SHA-256 hashes that indicate tampering), nested duplicate packages pinned at conflicting versions, and yanked/unpublished package versions still pinned in the lockfile. Supports package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, Pipfile.lock, Cargo.lock, go.sum. Generates a CI freshness gate command. Catches supply-chain attack surface that standard vulnerability scanners miss. Zero external API — pure local file analysis. Triggers on "lockfile audit", "package-lock security", "phantom dependency", "lockfile drift", "supply chain check", "integrity hash", "/lock-file-audit". license: Apache-2.0 homepage: https://canlah.ai metadata: author: Canlah AI version: "1.0.3" tags: - security - lockfile - supply-chain - npm - python - cargo - developer-tools - dependencies - integrity --- # Lockfile Security Auditor The XZ Utils backdoor wasn't in a CVE database. It was introduced through a dependency that looked legitimate until it wasn't. Your lockfile is a contract with the supply chain. This skill reads it for what it actually says — not just which packages are there, but whether the lockfile matches your manifest, whether integrity hashes are intact, and whether any entries were quietly swapped, added, or tampered with. **Supports npm, yarn, pnpm, poetry, pip, cargo, go modules. Zero external API.** --- ## Trigger Phrases - "lockfile audit", "package-lock security", "lock file check" - "phantom dependency", "lockfile drift", "supply chain check" - "integrity hash", "package-lock.json tampering" - "yarn.lock audit", "Cargo.lock check", "go.sum audit" - "lockfile out of sync", "stale lockfile" - "/lock-file-audit" --- ## How to Provide Input ```bash # Option 1: Audit current directory (auto-detect lockfile) /lock-file-audit # Option 2: Specific lockfile /lock-file-audit package-lock.json /lock-file-audit poetry.lock /lock-file-audit Cargo.lock /lock-file-audit go.sum # Option 3: Specific checks only /lock-file-audit --check phantom # Phantom dependencies only /lock-file-audit --check drift # Manifest/lockfile sync only /lock-file-audit --check integrity # Hash anomalies only /lock-file-audit --check duplicates # Conflicting version pins # Option 4: Generate CI gate script /lock-file-audit --ci-config # Option 5: Full audit with detailed output /lock-file-audit --verbose ``` --- ## Step 1: Detect Lockfile Type ```bash python3 -c " import os from pathlib import Path lockfiles = { 'package-lock.json': 'npm', 'yarn.lock': 'yarn', 'pnpm-lock.yaml': 'pnpm', 'poetry.lock': 'poetry', 'Pipfile.lock': 'pipenv', 'Cargo.lock': 'cargo', 'go.sum': 'go modules', } manifests = { 'npm': 'package.json', 'yarn': 'package.json', 'pnpm': 'package.json', 'poetry': 'pyproject.toml', 'pipenv': 'Pipfile', 'cargo': 'Cargo.toml', 'go modules': 'go.mod', } found = [] for fname, ecosystem in lockfiles.items(): if os.path.exists(fname): size = os.path.getsize(fname) manifest = manifests[ecosystem] has_manifest = os.path.exists(manifest) found.append((fname, ecosystem, size, manifest, has_manifest)) if found: for fname, ecosystem, size, manifest, has_manifest in found: status = '✅' if has_manifest else '⚠️ manifest missing' print(f'{ecosystem}: {fname} ({size/1024:.0f} KB) — manifest: {manifest} {status}') else: print('No lockfiles found in current directory.') print('Expected: package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, Pipfile.lock, Cargo.lock, go.sum') " ``` --- ## Step 2: Detect Phantom Dependencies Phantom dependencies are packages that appear in the lockfile but have no corresponding direct or indirect entry in the manifest. In npm, this is common when someone manually edits package-lock.json or when a package removes itself from its own dependencies but forgets to update the lockfile. ```python import json from pathlib import Path def audit_npm_phantoms(lockfile_path='package-lock.json', manifest_path='package.json'): """Find packages in package-lock.json not traceable to package.json.""" with open(lockfile_path) as f: lock = json.load(f) with open(manifest_path) as f: manifest = json.load(f) # All declared deps (direct) declared = set() for section in ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']: declared.update(manifest.get(section, {}).keys()) # All packages in lockfile (v2/v3 format) lock_packages = set() if 'packages' in lock: # npm v7+ (lockfileVersion 2/3) for path_key in lock['packages']: if path_key == '': # root entry continue # Extract package name: "node_modules/pkg" → "pkg" # or "node_modules/@scope/pkg" → "@scope/pkg" name = path_key.replace('node_modules/', '', 1) if '/' in name and not name.startswith('@'): # Nested: node_modules/a/node_modules/b → take the last segment name = name.split('/node_modules/')[-1] lock_packages.add(name) elif 'dependencies' in lock: # npm v1 (lockfileVersion 1) def collect_v1(deps, collected): for name, info in deps.items(): collected.add(name) if 'dependencies' in info: collect_v1(info['dependencies'], collected) collect_v1(lock.get('dependencies', {}), lock_packages) # Build full reachable set from manifest (BFS through lockfile deps) def get_lock_deps(pkg_name): """Get declared deps of a package from lockfile.""" pkg_info = lock.get('packages', {}).get(f'node_modules/{pkg_name}', {}) return set(pkg_info.get('dependencies', {}).keys()) | \ set(pkg_info.get('peerDependencies', {}).keys()) reachable = set(declared) queue = list(declared) while queue: pkg = queue.pop() for dep in get_lock_deps(pkg): if dep not in reachable: reachable.add(dep) queue.append(dep) # Phantoms: in lockfile but not reachable from manifest phantoms = lock_packages - reachable return sorted(phantoms), sorted(reachable), sorted(lock_packages) def audit_cargo_phantoms(lockfile_path='Cargo.lock', manifest_path='Cargo.toml'): """Find packages in Cargo.lock not in dependency tree of Cargo.toml.""" import re lock_content = Path(lockfile_path).read_text() manifest_content = Path(manifest_path).read_text() # Parse Cargo.lock packages lock_packages = set(re.findall(r'^name = "([^"]+)"', lock_content, re.MULTILINE)) # Parse Cargo.toml declared deps declared = set(re.findall(r'^([a-z_-]+)\s*=', manifest_content, re.MULTILINE)) # Also parse [dependencies] sections dep_section = re.findall(r'^\[(?:dependencies|dev-dependencies|build-dependencies)\]\n(.*?)(?=\n\[|\Z)', manifest_content, re.MULTILINE | re.DOTALL) for section in dep_section: declared.update(re.findall(r'^([a-z_-]+)\s*=', section, re.MULTILINE)) phantoms = lock_packages - declared return sorted(phantoms) ``` --- ## Step 3: Detect Lockfile Drift Lockfile drift happens when someone edits the manifest (`package.json`, `pyproject.toml`, etc.) without running `npm install` / `poetry lock`. The lockfile then describes a different dependency tree than what the manifest requests. ```python import json import re from pathlib import Path def check_npm_drift(lockfile_path='package-lock.json', manifest_path='package.json'): """Check if package-lock.json is in sync with package.json.""" with open(lockfile_path) as f: lock = json.load(f) with open(manifest_path) as f: manifest = json.load(f) issues = [] # The lockfile's root 'packages[""]' should mirror the manifest's deps root_lock = lock.get('packages', {}).get('', {}) lock_deps = {**root_lock.get('dependencies', {}), **root_lock.get('devDependencies', {}), **root_lock.get('peerDependencies', {})} manifest_deps = {**manifest.get('dependencies', {}), **manifest.get('devDependencies', {}), **manifest.get('peerDependencies', {})} # In manifest but not in lockfile root missing_from_lock = set(manifest_deps) - set(lock_deps) for pkg in sorted(missing_from_lock): issues.append({ 'type': 'MISSING_FROM_LOCK', 'package': pkg, 'manifest_version': manifest_deps[pkg], 'description': f'"{pkg}" in package.json but not in lockfile root entry — lockfile needs update.', 'fix': 'Run: npm install', }) # In lockfile root but not in manifest extra_in_lock = set(lock_deps) - set(manifest_deps) for pkg in sorted(extra_in_lock): issues.append({ 'type': 'EXTRA_IN_LOCK', 'package': pkg, 'lock_version': lock_deps[pkg], 'description': f'"{pkg}" in lockfile root but not in package.json — ghost entry.', 'fix': 'Run: npm install to regenerate lockfile, or manually remove entry.', }) # Version range mismatch for pkg in set(manifest_deps) & set(lock_deps): manifest_range = manifest_deps[pkg] lock_range = lock_deps.get(pkg, '') # Only flag if manifest has a strict pinned version that differs from lock if manifest_range != lock_range and not manifest_range.startswith('^') and not manifest_range.startswith('~'): issues.append({ 'type': 'VERSION_MISMATCH', 'package': pkg, 'manifest_version': manifest_range, 'lock_version': lock_range, 'description': f'"{pkg}": manifest pins {manifest_range} but lockfile has {lock_range}.', 'fix': f'Run: npm install {pkg}@{manifest_range} to align lockfile.', }) # Check lockfileVersion matches npm version lock_version = lock.get('lockfileVersion', 'unknown') return issues, lock_version def check_poetry_drift(lockfile_path='poetry.lock', manifest_path='pyproject.toml'): """Check if poetry.lock is in sync with pyproject.toml.""" import re lock_content = Path(lockfile_path).read_text() manifest_content = Path(manifest_path).read_text() # poetry.lock has a content-hash that poetry uses for drift detection content_hash_match = re.search(r'^content-hash = "([a-f0-9]+)"', lock_content, re.MULTILINE) if content_hash_match: stored_hash = content_hash_match.group(1) # Recalculate hash from pyproject.toml import hashlib computed = hashlib.sha256(manifest_content.encode()).hexdigest() if stored_hash != computed: return [{ 'type': 'CONTENT_HASH_MISMATCH', 'description': 'poetry.lock content-hash does not match current pyproject.toml.', 'stored_hash': stored_hash[:16] + '...', 'computed_hash': computed[:16] + '...', 'fix': 'Run: poetry lock --no-update', 'severity': 'HIGH', }] return [] ``` --- ## Step 4: Integrity Hash Audit ```python import json import re import hashlib from pathlib import Path def audit_npm_integrity(lockfile_path='package-lock.json'): """Check for missing, malformed, or suspicious integrity hashes.""" with open(lockfile_path) as f: lock = json.load(f) issues = [] packages = lock.get('packages', lock.get('dependencies', {})) VALID_HASH_PREFIXES = {'sha512-', 'sha256-', 'sha1-'} for pkg_path, pkg_info in packages.items(): if pkg_path == '': continue pkg_name = pkg_path.replace('node_modules/', '').split('/node_modules/')[-1] integrity = pkg_info.get('integrity', '') version = pkg_info.get('version', 'unknown') # Missing integrity hash if not integrity: issues.append({ 'package': pkg_name, 'version': version, 'type': 'MISSING_INTEGRITY', 'description': f'{pkg_name}@{version} has no integrity hash — tamper detection disabled.', 'severity': 'MEDIUM', 'fix': 'Run: npm install to regenerate integrity hashes.', }) continue # Malformed hash (wrong prefix or length) has_valid_prefix = any(integrity.startswith(p) for p in VALID_HASH_PREFIXES) if not has_valid_prefix: issues.append({ 'package': pkg_name, 'version': version, 'type': 'MALFORMED_HASH', 'description': f'{pkg_name}@{version} integrity hash has unknown prefix: {integrity[:20]}...', 'severity': 'HIGH', 'fix': 'Verify package source and regenerate lockfile: rm package-lock.json && npm install', }) # SHA-1 only (weak, collision-vulnerable) if integrity.startswith('sha1-') and not any( pkg_info.get('integrity', '').count(p) > 0 for p in ['sha512-', 'sha256-'] ): issues.append({ 'package': pkg_name, 'version': version, 'type': 'WEAK_HASH_ALGORITHM', 'description': f'{pkg_name}@{version} uses SHA-1 integrity only — SHA-512 preferred.', 'severity': 'LOW', 'fix': 'npm v7+ generates SHA-512. Run: npm install --lockfile-version 2', }) return issues def audit_go_sum(go_sum_path='go.sum'): """Check go.sum for anomalies.""" content = Path(go_sum_path).read_text() issues = [] seen = {} for line in content.splitlines(): if not line.strip(): continue parts = line.split() if len(parts) != 3: issues.append({ 'type': 'MALFORMED_ENTRY', 'line': line, 'description': 'go.sum entry has wrong number of fields (expected: module version hash)', 'severity': 'HIGH', }) continue module, version, hash_val = parts # Check hash format: h1:<base64> if not hash_val.startswith('h1:'): issues.append({ 'type': 'UNKNOWN_HASH_ALGORITHM', 'module': module, 'version': version, 'hash': hash_val[:20], 'description': f'Unknown hash algorithm in go.sum: {hash_val[:10]}... (expected h1:)', 'severity': 'HIGH', }) # Check for duplicate module+version with different hash (tampering signal) key = (module, version) if key in seen and seen[key] != hash_val: issues.append({ 'type': 'DUPLICATE_CONFLICTING_HASH', 'module': module, 'version': version, 'hash1': seen[key][:30], 'hash2': hash_val[:30], 'description': f'{module}@{version} has TWO different hashes in go.sum — possible tampering!', 'severity': 'CRITICAL', }) seen[key] = hash_val return issues ``` --- ## Step 5: Detect Conflicting Version Pins ```python import json import re from collections import defaultdict def find_npm_version_conflicts(lockfile_path='package-lock.json'): """Find packages pinned at multiple different versions in the lockfile.""" with open(lockfile_path) as f: lock = json.load(f) pkg_versions = defaultdict(list) for pkg_path, pkg_info in lock.get('packages', {}).items(): if pkg_path == '': continue name = pkg_path.replace('node_modules/', '').split('/node_modules/')[-1] version = pkg_info.get('version', 'unknown') pkg_versions[name].append((pkg_path, version)) conflicts = { name: versions for name, versions in pkg_versions.items() if len(set(v for _, v in versions)) > 1 } return dict(sorted(conflicts.items(), key=lambda x: len(x[1]), reverse=True)) def find_cargo_version_conflicts(lockfile_path='Cargo.lock'): """Find crates pinned at multiple versions.""" import re content = Path(lockfile_path).read_text() crate_versions = defaultdict(set) # Parse [[package]] sections package_blocks = re.findall( r'\[\[package\]\]\nname = "([^"]+)"\nversion = "([^"]+)"', content ) for name, version in package_blocks: crate_versions[name].add(version) return {name: sorted(versions) for name, versions in crate_versions.items() if len(versions) > 1} ``` --- ## Step 6: CI Freshness Gate ```python def generate_ci_gate(ecosystem): """Generate a CI script to enforce lockfile freshness.""" scripts = { 'npm': ''' # Check that package-lock.json is not stale (generated within 30 days) python3 -c " import os, sys, time lockfile = 'package-lock.json' max_age_days = 30 if not os.path.exists(lockfile): print('FAIL: package-lock.json not found') sys.exit(1) age_days = (time.time() - os.path.getmtime(lockfile)) / 86400 if age_days > max_age_days: print(f'FAIL: package-lock.json is {age_days:.0f} days old (limit: {max_age_days})') print('Run: npm install && git add package-lock.json') sys.exit(1) print(f'PASS: package-lock.json is {age_days:.0f} days old') " # Check lockfile is in sync with package.json npm ci --dry-run 2>&1 | grep -q "up to date" || (echo "FAIL: lockfile drift detected" && npm install --dry-run && exit 1) ''', 'poetry': ''' # Check poetry.lock is in sync with pyproject.toml poetry lock --check 2>&1 # Returns exit 1 if poetry.lock is not up to date ''', 'cargo': ''' # Check Cargo.lock is in sync with Cargo.toml cargo update --dry-run 2>&1 | grep -q "Updating" && echo "WARNING: Cargo.lock has available updates" || echo "PASS: Cargo.lock is current" # Verify Cargo.lock is committed (required for applications, optional for libraries) git show HEAD:Cargo.lock > /dev/null 2>&1 || echo "WARNING: Cargo.lock not committed to git" ''', 'go': ''' # Verify go.sum is consistent with go.mod go mod verify 2>&1 # Exits 1 if any module in go.sum has a hash mismatch vs downloaded module ''', } return scripts.get(ecosystem, '# CI gate not available for this ecosystem') ``` --- ## Step 7: Output Report ```markdown ## Lockfile Security Audit Project: my-app | Ecosystem: npm | Lockfile: package-lock.json (847 packages) --- ### Summary | Check | Status | Count | |-------|--------|-------| | Phantom dependencies | 🔴 Found | 3 packages | | Lockfile drift | 🟠 Detected | 2 manifest/lock mismatches | | Integrity hash anomalies | 🟡 Found | 1 SHA-1 only package | | Version conflicts | 🟠 Found | 12 packages at 2+ versions | | Stale lockfile | ✅ Fresh | 4 days old | --- ### 🔴 Phantom Dependencies (3) These packages are in `package-lock.json` but cannot be traced back to any entry in `package.json`: ``` package-lock.json contains: lodash-es@4.17.21 ← not declared in package.json, not a transitive dep of any declared package uuid@8.3.2 ← was removed from package.json 3 months ago, still in lockfile debug@4.3.4 ← appears in lockfile root but not resolvable from manifest graph ``` **Why this matters:** Phantom entries could indicate: - A dependency was removed from package.json but not from lockfile (stale — low risk) - A package was directly injected into lockfile (high risk — supply chain concern) - A `npm install <pkg>` was run but package.json was not updated (incomplete change) **Fix:** ```bash # Regenerate lockfile from scratch: rm package-lock.json npm install # Diff the result — if new lockfile differs, review every change git diff package-lock.json ``` --- ### 🟠 Lockfile Drift (2 mismatches) | Package | In package.json | In lockfile | Issue | |---------|----------------|-------------|-------| | `react` | `^18.3.0` | not in root | Added to package.json, `npm install` not run | | `jest` | removed | `^29.0.0` still in lockfile root | Removed from package.json, lockfile not updated | **Fix:** `npm install` — this regenerates the lockfile to match the manifest. --- ### 🟠 Version Conflicts (12 packages at multiple versions) The top 5 most-duplicated packages: | Package | Versions Pinned | Copies in Bundle | |---------|----------------|-----------------| | `lodash` | 4.17.15, 4.17.21 | 2 (adds ~140 KB) | | `tslib` | 1.14.1, 2.6.2 | 2 | | `semver` | 6.3.1, 7.5.4 | 3 copies | | `debug` | 3.2.7, 4.3.4 | 2 copies | | `ms` | 2.0.0, 2.1.3 | 2 copies | **Why this matters:** Multiple versions = bigger bundle, potential behavior inconsistency, harder security patching. **Fix for lodash:** ```bash npm dedupe lodash # Or force all consumers to use 4.17.21: # Add to package.json: "overrides": { "lodash": "^4.17.21" } ``` --- ### 🟡 Weak Integrity Hash (1 package) ``` mkdirp@0.5.5 — integrity: sha1-2j4w5... (SHA-1 only) ``` SHA-1 has known collision vulnerabilities. This package was published before npm adopted SHA-512. Not an immediate threat, but consider upgrading to a version with SHA-512 hashes. --- ### CI Freshness Gate Add to `.github/workflows/ci.yml`: ```yaml - name: Verify lockfile integrity run: | # Fail if lockfile is stale (>30 days) or out of sync npm ci --dry-run # npm ci fails if lockfile doesn't match package.json exactly ``` Or add pre-commit hook: ```bash # .git/hooks/pre-commit #!/bin/sh if git diff --cached --name-only | grep -q "package.json"; then if ! git diff --cached --name-only | grep -q "package-lock.json"; then echo "⚠️ package.json changed but package-lock.json was not updated." echo "Run: npm install && git add package-lock.json" exit 1 fi fi ``` --- ### Supply Chain Risk Assessment Based on this audit: | Risk | Assessment | |------|-----------| | Tampered lockfile | Low — integrity hashes present for 846/847 packages | | Phantom injection | Medium — 3 phantom packages need investigation | | Outdated lockfile attack surface | Low — lockfile is 4 days old | | Duplicate version confusion | Low — version conflicts are from transitive deps, not direct | **Recommended action:** Regenerate lockfile (`rm package-lock.json && npm install`) and review the diff before committing. ``` --- ## Quick Mode Output ``` Lockfile Audit: my-app (npm, package-lock.json, 847 packages) 🔴 3 phantom dependencies — packages in lockfile with no manifest trace 🟠 2 drift issues — package.json changed without npm install 🟠 12 version conflicts — lodash 4.17.15+4.17.21, semver ×3, debug ×2... 🟡 1 SHA-1-only integrity hash (mkdirp@0.5.5) ✅ No integrity hash tampering detected ✅ Lockfile is 4 days old Priority: Regenerate lockfile (rm package-lock.json && npm install) → review diff ``` --- ## Author **[Canlah AI](https://canlah.ai)** — Run performance marketing without breaking your brand. - GitHub: [github.com/PHY041](https://github.com/PHY041) - All Skills: [clawhub.ai/PHY041](https://clawhub.ai/PHY041)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)