From 3111df26feb82be8d1f9efdbcd6cfcfc1c2f5b50 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:15:04 -0400 Subject: [PATCH 1/7] update to new endpoint --- .../package-vulnerability-scanner/main.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index 902dfa17..b49c3e3e 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -31,14 +31,42 @@ async def get_vulnerabilities(): async def fetch_repo_vulns(repo): async with httpx.AsyncClient() as client: - url = f"https://packagemanager.posit.co/__api__/repos/{repo}/vulns" - response = await client.get(url) + # Use POST request with /filter/packages endpoint + url = f"https://packagemanager.posit.co/__api__/filter/packages" + payload = { + "repo": repo, + "has_vulns": True, + "omit_downloads": True, + "omit_dependencies": True + } + response = await client.post(url, json=payload) response.raise_for_status() return repo, response.json() tasks = [fetch_repo_vulns(repo) for repo in repositories] - for repo, data in await asyncio.gather(*tasks): - results[repo] = data + all_results = await asyncio.gather(*tasks) + + for repo, data in all_results: + # Transform package results into vulnerability map + # The /filter/packages endpoint returns packages, each potentially with vulns array + vuln_map = {} + + # Handle array of packages + if isinstance(data, list): + packages = data + # Handle object with packages nested inside + elif isinstance(data, dict): + packages = data if len(data) <= 1 and any(k in data for k in ['vulns', 'name']) else [data] + else: + packages = [] + + # Extract vulnerability data from packages + for pkg in packages: + if isinstance(pkg, dict) and pkg.get('vulns'): + pkg_name = pkg.get('name', 'unknown') + vuln_map[pkg_name] = pkg['vulns'] + + results[repo] = vuln_map return results From aae5c1ae75f405a3113cc85d1760bf8c1ae11d16 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:43:22 -0400 Subject: [PATCH 2/7] hacky way to revive the app --- .../package-vulnerability-scanner/main.py | 35 ++++++++++--------- .../package-lock.json | 7 +++- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index b49c3e3e..fc90a6fe 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -30,6 +30,7 @@ async def get_vulnerabilities(): results = {} async def fetch_repo_vulns(repo): + import json async with httpx.AsyncClient() as client: # Use POST request with /filter/packages endpoint url = f"https://packagemanager.posit.co/__api__/filter/packages" @@ -41,33 +42,33 @@ async def fetch_repo_vulns(repo): } response = await client.post(url, json=payload) response.raise_for_status() - return repo, response.json() + # API returns newline-delimited JSON (NDJSON), parse each line + packages = [] + for line in response.text.strip().split('\n'): + if line: + packages.append(json.loads(line)) + return repo, packages tasks = [fetch_repo_vulns(repo) for repo in repositories] all_results = await asyncio.gather(*tasks) for repo, data in all_results: # Transform package results into vulnerability map - # The /filter/packages endpoint returns packages, each potentially with vulns array vuln_map = {} - - # Handle array of packages - if isinstance(data, list): - packages = data - # Handle object with packages nested inside - elif isinstance(data, dict): - packages = data if len(data) <= 1 and any(k in data for k in ['vulns', 'name']) else [data] - else: - packages = [] - + packages = data if isinstance(data, list) else [] + # Extract vulnerability data from packages + # Use 'project_name' as key, lowercased to match installed package names + # (Connect returns lowercase names like 'beaker', but API has 'Beaker') for pkg in packages: - if isinstance(pkg, dict) and pkg.get('vulns'): - pkg_name = pkg.get('name', 'unknown') - vuln_map[pkg_name] = pkg['vulns'] - + if isinstance(pkg, dict): + pkg_name = (pkg.get('project_name') or pkg.get('name', 'unknown')).lower() + vulns = pkg.get('vulns') or [] + if vulns: + vuln_map[pkg_name] = vulns + results[repo] = vuln_map - + return results @app.get("/api/user") diff --git a/extensions/package-vulnerability-scanner/package-lock.json b/extensions/package-vulnerability-scanner/package-lock.json index 7ae83213..827b6099 100644 --- a/extensions/package-vulnerability-scanner/package-lock.json +++ b/extensions/package-vulnerability-scanner/package-lock.json @@ -1937,6 +1937,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2116,7 +2117,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -2153,6 +2155,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2179,6 +2182,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2260,6 +2264,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", From e41de6055f41fd0c8ddb46e16c60463e0c98c575 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:05:08 -0400 Subject: [PATCH 3/7] clean up from claude guessing --- .../package-vulnerability-scanner/main.py | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index fc90a6fe..c37de968 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -1,3 +1,7 @@ +import asyncio +import json + +import httpx from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from posit import connect @@ -6,12 +10,14 @@ client = connect.Client() + @app.get("/api/content") async def search_content(show_all: bool = False): if show_all: return client.content.find() return client.me.content.find() + @app.get("/api/packages/{guid}") async def get_packages(guid: str): try: @@ -19,60 +25,43 @@ async def get_packages(guid: str): packages = list(content.packages) return packages except Exception as e: - raise HTTPException(status_code=404, detail=f"Content not found or error fetching packages: {str(e)}") - + raise HTTPException( + status_code=404, + detail=f"Content not found or error fetching packages: {str(e)}", + ) + + @app.get("/api/vulns") async def get_vulnerabilities(): - import httpx - import asyncio - repositories = ["pypi", "cran"] results = {} - + async def fetch_repo_vulns(repo): - import json - async with httpx.AsyncClient() as client: - # Use POST request with /filter/packages endpoint - url = f"https://packagemanager.posit.co/__api__/filter/packages" + async with httpx.AsyncClient() as http: + url = "https://packagemanager.posit.co/__api__/filter/packages" payload = { "repo": repo, "has_vulns": True, "omit_downloads": True, - "omit_dependencies": True + "omit_dependencies": True, } - response = await client.post(url, json=payload) + response = await http.post(url, json=payload) response.raise_for_status() - # API returns newline-delimited JSON (NDJSON), parse each line - packages = [] - for line in response.text.strip().split('\n'): - if line: - packages.append(json.loads(line)) + packages = [ + json.loads(line) for line in response.text.strip().split("\n") if line + ] return repo, packages - + tasks = [fetch_repo_vulns(repo) for repo in repositories] - all_results = await asyncio.gather(*tasks) - - for repo, data in all_results: - # Transform package results into vulnerability map - vuln_map = {} - packages = data if isinstance(data, list) else [] - - # Extract vulnerability data from packages - # Use 'project_name' as key, lowercased to match installed package names - # (Connect returns lowercase names like 'beaker', but API has 'Beaker') - for pkg in packages: - if isinstance(pkg, dict): - pkg_name = (pkg.get('project_name') or pkg.get('name', 'unknown')).lower() - vulns = pkg.get('vulns') or [] - if vulns: - vuln_map[pkg_name] = vulns - - results[repo] = vuln_map + for repo, packages in await asyncio.gather(*tasks): + results[repo] = {pkg["name"]: pkg["vulns"] for pkg in packages} return results + @app.get("/api/user") async def get_current_user(): return client.me + app.mount("/", StaticFiles(directory="dist", html=True), name="static") From dd8f9f4ff05613665d43ab9defedab66da7ff6a4 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:53:49 -0400 Subject: [PATCH 4/7] information for legacy versions in code comment --- extensions/package-vulnerability-scanner/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index c37de968..7259c4f6 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -38,6 +38,14 @@ async def get_vulnerabilities(): async def fetch_repo_vulns(repo): async with httpx.AsyncClient() as http: + # This fetches vulnerabilities from the public Posit Package Manager, + # which is always up to date. If customizing this to reference your + # own PPM instance, note that the has_vulns parameter requires + # PPM 2026.04 or later. Older versions may need to use the vulns + # parameter instead, or the legacy endpoint: + # + # url = f"https://your-ppm-instance/__api__/repos/{repo}/vulns" + # response = await http.get(url) url = "https://packagemanager.posit.co/__api__/filter/packages" payload = { "repo": repo, From 2d0a7c8752ffdb4e1dad32d7b0ded1457628916d Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:59:51 -0400 Subject: [PATCH 5/7] more content for code comment --- extensions/package-vulnerability-scanner/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index 7259c4f6..884a34a3 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -46,6 +46,10 @@ async def fetch_repo_vulns(repo): # # url = f"https://your-ppm-instance/__api__/repos/{repo}/vulns" # response = await http.get(url) + # return repo, response.json() + # + # for repo, data in await asyncio.gather(*tasks): + # results[repo] = data url = "https://packagemanager.posit.co/__api__/filter/packages" payload = { "repo": repo, From 168ff82a373c8a91c0eb73d1479cbfd898231b2e Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:02:01 -0400 Subject: [PATCH 6/7] indentation, missing line --- extensions/package-vulnerability-scanner/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index 884a34a3..423eab7e 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -48,8 +48,9 @@ async def fetch_repo_vulns(repo): # response = await http.get(url) # return repo, response.json() # - # for repo, data in await asyncio.gather(*tasks): - # results[repo] = data + # tasks = [fetch_repo_vulns(repo) for repo in repositories] + # for repo, data in await asyncio.gather(*tasks): + # results[repo] = data url = "https://packagemanager.posit.co/__api__/filter/packages" payload = { "repo": repo, From 89c461e86e8ddf32c0a24c0e18a99889527d0680 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:02:42 -0400 Subject: [PATCH 7/7] indentation --- extensions/package-vulnerability-scanner/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/package-vulnerability-scanner/main.py b/extensions/package-vulnerability-scanner/main.py index 423eab7e..344bcc76 100644 --- a/extensions/package-vulnerability-scanner/main.py +++ b/extensions/package-vulnerability-scanner/main.py @@ -44,9 +44,9 @@ async def fetch_repo_vulns(repo): # PPM 2026.04 or later. Older versions may need to use the vulns # parameter instead, or the legacy endpoint: # - # url = f"https://your-ppm-instance/__api__/repos/{repo}/vulns" - # response = await http.get(url) - # return repo, response.json() + # url = f"https://your-ppm-instance/__api__/repos/{repo}/vulns" + # response = await http.get(url) + # return repo, response.json() # # tasks = [fetch_repo_vulns(repo) for repo in repositories] # for repo, data in await asyncio.gather(*tasks):