Files
stack/dev/scripts/pkg_drift.py
kert 29b302cdf0
Some checks failed
CI / skinny-install (aco) (push) Successful in 46s
CI / skinny-install (api) (push) Successful in 28s
CI / skinny-install (bcda) (push) Successful in 26s
CI / skinny-install (bib) (push) Successful in 24s
CI / skinny-install (bls) (push) Successful in 28s
CI / skinny-install (ccw) (push) Successful in 31s
CI / skinny-install (cli) (push) Successful in 26s
CI / skinny-install (cms) (push) Successful in 26s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (pfs) (push) Successful in 26s
CI / skinny-install (rex) (push) Successful in 24s
CI / lint-test (push) Successful in 5m50s
Infra CI / notebooks (push) Successful in 1m14s
Infra CI / zotero (push) Failing after 5s
Infra CI / docs (push) Successful in 6s
Infra CI / api (push) Successful in 13s
Infra CI / mc (push) Successful in 7s
Deploy / build-scan-report (push) Successful in 5m6s
feat: package supply chain — inventory, mirrors, drift, vuln scanning (refs #159–#168)
Cherry-picked from feat/pkg-supply-chain, adapted for infra/ tree layout:

- dev/scripts/pkg_inventory.py — scans Dockerfiles, pyproject.toml, CI
  workflows, and shell scripts to build a unified package manifest
- dev/scripts/pkg_mirror_sync.py — syncs PyPI/APK/npm packages to
  Gitea package registry (replaces devpi/apt-cacher-ng)
- dev/scripts/pkg_drift.py — compares mirror contents against manifest,
  flags missing or stale packages
- dev/scripts/pkg_issues.py — auto-creates Gitea issues for drift and
  CVE findings
- dev/scripts/test_network_isolation.sh — verifies containers can't
  reach the internet except through mirrors
- dev/pipelines/pkg-supply-chain.yml — CI-agnostic pipeline spec
- .gitea/workflows/pkg-supply-chain.yml — daily + push-triggered CI job
- PYPI_INDEX_URL build arg added to api and notebooks Dockerfiles
2026-03-24 17:02:30 -04:00

143 lines
4.5 KiB
Python

"""Detect drift between the package manifest and what is actually used.
Compares data/pkg-manifest.json against the repo source to detect:
- packages in manifest but missing from source (phantom)
- packages in source but missing from manifest (untracked)
Usage:
uv run python dev/scripts/pkg_drift.py # report + exit code
uv run python dev/scripts/pkg_drift.py --json # JSON output
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MANIFEST_PATH = ROOT / "data" / "pkg-manifest.json"
def load_manifest() -> dict:
if not MANIFEST_PATH.exists():
print(f"ERROR: {MANIFEST_PATH} not found. Run pkg_inventory.py first.")
raise SystemExit(2)
return json.loads(MANIFEST_PATH.read_text())
def scan_fresh() -> dict:
"""Run a fresh inventory scan and return the result."""
from pkg_inventory import scan
return scan()
def diff_lists(manifest_items: list, fresh_items: list, key: str = "name") -> dict:
"""Compare two lists of dicts by a key field."""
if manifest_items and isinstance(manifest_items[0], dict):
m_set = {item[key] for item in manifest_items}
f_set = {item[key] for item in fresh_items}
else:
m_set = set(manifest_items)
f_set = set(fresh_items)
return {
"phantom": sorted(m_set - f_set),
"untracked": sorted(f_set - m_set),
}
def check_drift() -> dict:
manifest = load_manifest()
fresh = scan_fresh()
drift: dict = {"apt": {}, "apk": {}, "pypi": {}, "npm": {}, "has_drift": False}
# apt
all_apt_manifest = set()
for pkgs in manifest.get("apt", {}).values():
all_apt_manifest.update(pkgs)
all_apt_fresh = set()
for pkgs in fresh.get("apt", {}).values():
all_apt_fresh.update(pkgs)
apt_diff = {
"phantom": sorted(all_apt_manifest - all_apt_fresh),
"untracked": sorted(all_apt_fresh - all_apt_manifest),
}
if apt_diff["phantom"] or apt_diff["untracked"]:
drift["apt"] = apt_diff
drift["has_drift"] = True
# apk
all_apk_manifest = set()
for pkgs in manifest.get("apk", {}).values():
all_apk_manifest.update(pkgs)
all_apk_fresh = set()
for pkgs in fresh.get("apk", {}).values():
all_apk_fresh.update(pkgs)
apk_diff = {
"phantom": sorted(all_apk_manifest - all_apk_fresh),
"untracked": sorted(all_apk_fresh - all_apk_manifest),
}
if apk_diff["phantom"] or apk_diff["untracked"]:
drift["apk"] = apk_diff
drift["has_drift"] = True
# pypi (compare all sections)
for section in ("project_prod", "project_dev", "notebook", "dockerfile_adhoc"):
d = diff_lists(
manifest.get("pypi", {}).get(section, []),
fresh.get("pypi", {}).get(section, []),
)
if d["phantom"] or d["untracked"]:
drift["pypi"][section] = d
drift["has_drift"] = True
# npm
d = diff_lists(
manifest.get("npm", []),
fresh.get("npm", []),
)
if d["phantom"] or d["untracked"]:
drift["npm"] = d
drift["has_drift"] = True
return drift
def main() -> None:
drift = check_drift()
if "--json" in sys.argv:
print(json.dumps(drift, indent=2))
else:
if not drift["has_drift"]:
print("No drift detected — manifest matches repo source.")
else:
print("DRIFT DETECTED:")
for pkg_type in ("apt", "apk", "pypi", "npm"):
section = drift[pkg_type]
if not section:
continue
if pkg_type == "pypi":
for sub, d in section.items():
if d.get("phantom"):
print(
f" pypi/{sub} phantom (in manifest, not in source): {d['phantom']}"
)
if d.get("untracked"):
print(
f" pypi/{sub} untracked (in source, not in manifest): {d['untracked']}"
)
else:
if section.get("phantom"):
print(f" {pkg_type} phantom: {section['phantom']}")
if section.get("untracked"):
print(f" {pkg_type} untracked: {section['untracked']}")
raise SystemExit(1 if drift["has_drift"] else 0)
if __name__ == "__main__":
main()