Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 45 additions & 14 deletions extensions/package-vulnerability-scanner/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import asyncio
import json

import httpx
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from posit import connect
Expand All @@ -6,44 +10,71 @@

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:
content = client.content.get(guid)
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):
async with httpx.AsyncClient() as client:
url = f"https://packagemanager.posit.co/__api__/repos/{repo}/vulns"
response = await client.get(url)
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)
# return repo, response.json()
#
# tasks = [fetch_repo_vulns(repo) for repo in repositories]
# for repo, data in await asyncio.gather(*tasks):
# results[repo] = data
Comment on lines +41 to +53
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonkeane What do you think about this as a solution to the other thread? I'm amendable to not leaving the old code and letting people sort that out on their own.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that works!

url = "https://packagemanager.posit.co/__api__/filter/packages"
payload = {
"repo": repo,
"has_vulns": True,
"omit_downloads": True,
"omit_dependencies": True,
}
response = await http.post(url, json=payload)
response.raise_for_status()
return repo, response.json()

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]
for repo, data in await asyncio.gather(*tasks):
results[repo] = data
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")
4 changes: 4 additions & 0 deletions extensions/package-vulnerability-scanner/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading