Skip to content

Commit 2df6f1f

Browse files
committed
fix: correct JS catalog to 4387 packages; revalidate all language scans
- JS catalog rebuilt from redirect_map tarball keys directly (no registry calls) — was 753 (broken), now 4387 with correct versions; 57 vulnerable / 169 CVEs - fetch_ldpov_npm.py rewritten: extracts versions from S3 redirect_map keys, eliminating registry API calls that caused rate-limiting and silent failures - fetch_ldpov_pypi.py: use S3 prefix listing for package discovery instead of root index URL (which returned 403) - Python re-scanned: 1730 / 31 vuln / 99 CVEs (consistent) - Java re-scanned: 5357 / 134 vuln / 337 CVEs (consistent)
1 parent 0026398 commit 2df6f1f

7 files changed

Lines changed: 67 additions & 75 deletions

File tree

public/data/ldpov/java/audit.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/data/ldpov/java/catalog.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/data/ldpov/javascript/audit.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/data/ldpov/javascript/catalog.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/data/ldpov/python/audit.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

scripts/fetch_ldpov_npm.py

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env python3
22
"""Fetch all package names + versions from the LDPoV npm catalog.
33
4-
Reads LDPOV_ORG_ID, LDPOV_CATALOG_TOKEN, NPM_URL from ../.env or environment.
5-
Uses S3 redirect_map for package discovery, then fetches versions from the registry.
4+
Reads LDPOV_ORG_ID from ../.env or environment.
5+
Fetches the redirect_map from S3 and extracts package names + versions
6+
directly from the tarball keys — no registry calls needed.
67
Writes public/data/ldpov/javascript/catalog.json.
78
"""
89

@@ -12,9 +13,7 @@
1213
import re
1314
import subprocess
1415
import urllib.parse
15-
from base64 import b64encode
16-
from concurrent.futures import ThreadPoolExecutor, as_completed
17-
from urllib.request import Request, urlopen
16+
from collections import defaultdict
1817

1918
_env: dict[str, str] = {}
2019
_env_path = os.path.join(os.path.dirname(__file__), "..", ".env")
@@ -25,76 +24,49 @@
2524
k, v = line.split("=", 1)
2625
_env[k.strip()] = v.strip()
2726

28-
ORG_ID = os.environ.get("LDPOV_ORG_ID", _env.get("LDPOV_ORG_ID", ""))
29-
TOKEN = os.environ.get("LDPOV_CATALOG_TOKEN", _env.get("LDPOV_CATALOG_TOKEN", ""))
30-
BASE_URL = os.environ.get("NPM_URL", _env.get("NPM_URL", ""))
27+
ORG_ID = os.environ.get("LDPOV_ORG_ID", _env.get("LDPOV_ORG_ID", ""))
3128

32-
if not ORG_ID or not TOKEN:
33-
raise SystemExit("ERROR: set LDPOV_ORG_ID and LDPOV_CATALOG_TOKEN in .env or environment")
34-
if not BASE_URL:
35-
raise SystemExit("ERROR: set NPM_URL in .env or environment")
29+
if not ORG_ID:
30+
raise SystemExit("ERROR: set LDPOV_ORG_ID in .env or environment")
3631

37-
AUTH = b64encode(f"x:{TOKEN}".encode()).decode()
32+
33+
def _version_key(v: str) -> list:
34+
return [int(x) if x.isdigit() else x for x in re.split(r"[.\-]", v.split("+")[0])]
3835

3936

40-
def get_package_list() -> list[str]:
37+
def main() -> None:
4138
s3_path = f"s3://curated-catalog/env=prod/org-id={ORG_ID}/repo-type=npm/redirect_map.json"
42-
print(f"Fetching npm package list from S3: {s3_path}", flush=True)
39+
print(f"Fetching npm redirect_map from S3: {s3_path}", flush=True)
4340
result = subprocess.run(
4441
["aws", "s3", "cp", s3_path, "-"],
4542
capture_output=True, text=True, check=True,
4643
)
4744
data = json.loads(result.stdout)
48-
names = sorted(
49-
{urllib.parse.unquote(k.split("/-/")[0]) for k in data},
50-
key=str.lower,
51-
)
52-
print(f"Found {len(names)} packages", flush=True)
53-
return names
54-
55-
56-
def fetch_versions(name: str) -> tuple[str, list[str]]:
57-
encoded = urllib.parse.quote(name, safe="@")
58-
url = f"{BASE_URL}{encoded}"
59-
try:
60-
req = Request(url, headers={"Authorization": f"Basic {AUTH}"})
61-
with urlopen(req, timeout=30) as r:
62-
doc = json.loads(r.read())
63-
versions = list(doc.get("versions", {}).keys())
64-
return name, sorted(versions, key=_version_key)
65-
except Exception:
66-
return name, []
67-
68-
69-
def _version_key(v: str) -> list:
70-
base = v.split("-")[0]
71-
nums = re.findall(r"\d+", base)
72-
return [int(n) for n in nums[:3]] + [0] * (3 - len(nums[:3]))
73-
74-
75-
def main() -> None:
76-
names = get_package_list()
77-
total = len(names)
78-
79-
results: list[dict] = []
80-
done = 0
81-
print(f"Fetching versions with 30 workers...", flush=True)
82-
83-
with ThreadPoolExecutor(max_workers=30) as pool:
84-
futures = {pool.submit(fetch_versions, n): n for n in names}
85-
for future in as_completed(futures):
86-
name, versions = future.result()
87-
if versions:
88-
results.append({"name": name, "versions": versions})
89-
done += 1
90-
if done % 500 == 0 or done == total:
91-
print(f" {done}/{total}", flush=True)
92-
93-
results.sort(key=lambda x: x["name"].lower())
45+
print(f"Found {len(data)} redirect entries", flush=True)
46+
47+
pkg_versions: dict[str, set[str]] = defaultdict(set)
48+
for raw_key in data:
49+
key = urllib.parse.unquote(raw_key)
50+
if "/-/" not in key:
51+
continue
52+
name_part, filename = key.split("/-/", 1)
53+
filename = filename.split("#")[0]
54+
if not filename.endswith(".tgz"):
55+
continue
56+
stem = filename[:-4]
57+
short_name = name_part.split("/")[-1]
58+
prefix = short_name + "-"
59+
if stem.startswith(prefix):
60+
pkg_versions[name_part].add(stem[len(prefix):])
61+
62+
packages = [
63+
{"name": name, "versions": sorted(versions, key=_version_key)}
64+
for name, versions in sorted(pkg_versions.items(), key=lambda x: x[0].lower())
65+
]
9466

9567
out = {
9668
"generated": datetime.datetime.utcnow().isoformat() + "Z",
97-
"packages": results,
69+
"packages": packages,
9870
}
9971

10072
out_path = os.path.join(
@@ -104,7 +76,7 @@ def main() -> None:
10476
with open(out_path, "w") as f:
10577
json.dump(out, f, separators=(",", ":"))
10678

107-
print(f"Wrote public/data/ldpov/javascript/catalog.json — {len(results)} packages ({total - len(results)} skipped, no versions)")
79+
print(f"Wrote public/data/ldpov/javascript/catalog.json — {len(packages)} packages")
10880

10981

11082
if __name__ == "__main__":

scripts/fetch_ldpov_pypi.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import json
1111
import os
1212
import re
13+
import subprocess
1314
from concurrent.futures import ThreadPoolExecutor, as_completed
1415
from urllib.parse import urljoin
1516
from urllib.request import Request, urlopen
@@ -68,20 +69,39 @@ def fetch_versions(pkg_name: str) -> tuple[str, list[str]]:
6869
return pkg_name, []
6970

7071

72+
def get_package_list_from_s3(org_id: str) -> list[str]:
73+
s3_path = f"s3://curated-catalog/env=prod/org-id={org_id}/repo-type=pypi/"
74+
print(f"Fetching PyPI package list from S3: {s3_path}", flush=True)
75+
result = subprocess.run(
76+
["aws", "s3", "ls", s3_path],
77+
capture_output=True, text=True, check=True,
78+
)
79+
packages = []
80+
for line in result.stdout.splitlines():
81+
# Lines look like: " PRE numpy/"
82+
parts = line.strip().split()
83+
if len(parts) == 2 and parts[0] == "PRE":
84+
packages.append(parts[1].rstrip("/"))
85+
return sorted(packages, key=str.lower)
86+
87+
7188
def main() -> None:
72-
print("Fetching LDPoV PyPI package list...", flush=True)
73-
html = get(BASE_URL)
74-
packages = [m.rstrip("/") for m in HREF_RE.findall(html) if m.endswith("/")]
89+
org_id = os.environ.get("LDPOV_ORG_ID", _env.get("LDPOV_ORG_ID", ""))
90+
if not org_id:
91+
raise SystemExit("ERROR: set LDPOV_ORG_ID in .env or environment")
92+
93+
packages = get_package_list_from_s3(org_id)
7594
total = len(packages)
76-
print(f"Found {total} packages. Fetching versions with 30 workers...", flush=True)
95+
print(f"Found {total} packages. Fetching versions with 10 workers...", flush=True)
7796

7897
results: list[dict] = []
7998
done = 0
80-
with ThreadPoolExecutor(max_workers=30) as pool:
99+
with ThreadPoolExecutor(max_workers=10) as pool:
81100
futures = {pool.submit(fetch_versions, pkg): pkg for pkg in packages}
82101
for future in as_completed(futures):
83102
name, versions = future.result()
84-
results.append({"name": name, "versions": versions})
103+
if versions:
104+
results.append({"name": name, "versions": versions})
85105
done += 1
86106
if done % 100 == 0 or done == total:
87107
print(f" {done}/{total}", flush=True)
@@ -101,7 +121,7 @@ def main() -> None:
101121
with open(out_path, "w") as f:
102122
json.dump(out, f, separators=(",", ":"))
103123

104-
print(f"Wrote public/data/ldpov/python/catalog.json — {total} packages")
124+
print(f"Wrote public/data/ldpov/python/catalog.json — {len(results)} packages ({total - len(results)} skipped, no versions)")
105125

106126

107127
if __name__ == "__main__":

0 commit comments

Comments
 (0)