forked from mirrors/pkg-proxy
655 lines
20 KiB
Go
655 lines
20 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
shared "github.com/git-pkgs/enrichment"
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
"github.com/git-pkgs/proxy/internal/enrichment"
|
|
"github.com/git-pkgs/purl"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
const (
|
|
maxBodySize = 1 << 20 // 1 MB
|
|
licenseCategoryUnknown = "unknown"
|
|
defaultSortBy = "hits"
|
|
)
|
|
|
|
// APIHandler provides REST endpoints for package enrichment data.
|
|
type APIHandler struct {
|
|
enrichment *enrichment.Service
|
|
ecosystems *shared.EcosystemsClient
|
|
db DBSearcher
|
|
}
|
|
|
|
// DBSearcher defines the interface for database search operations.
|
|
type DBSearcher interface {
|
|
SearchPackages(query string, ecosystem string, limit int, offset int) ([]database.SearchResult, error)
|
|
CountSearchResults(query string, ecosystem string) (int64, error)
|
|
ListCachedPackages(ecosystem string, sortBy string, limit int, offset int) ([]database.PackageListItem, error)
|
|
CountCachedPackages(ecosystem string) (int64, error)
|
|
}
|
|
|
|
// NewAPIHandler creates a new API handler with enrichment services.
|
|
func NewAPIHandler(svc *enrichment.Service, db DBSearcher) *APIHandler {
|
|
h := &APIHandler{
|
|
enrichment: svc,
|
|
db: db,
|
|
}
|
|
// Try to initialize ecosystems client for bulk lookups
|
|
if client, err := shared.NewEcosystemsClient(); err == nil {
|
|
h.ecosystems = client
|
|
}
|
|
return h
|
|
}
|
|
|
|
// PackageResponse contains enriched package metadata.
|
|
type PackageResponse struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
LatestVersion string `json:"latest_version,omitempty"`
|
|
License string `json:"license,omitempty"`
|
|
LicenseCategory string `json:"license_category,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
Repository string `json:"repository,omitempty"`
|
|
RegistryURL string `json:"registry_url,omitempty"`
|
|
}
|
|
|
|
// VersionResponse contains enriched version metadata.
|
|
type VersionResponse struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
License string `json:"license,omitempty"`
|
|
PublishedAt string `json:"published_at,omitempty"`
|
|
Integrity string `json:"integrity,omitempty"`
|
|
Yanked bool `json:"yanked"`
|
|
IsOutdated bool `json:"is_outdated"`
|
|
}
|
|
|
|
// VulnResponse contains vulnerability information.
|
|
type VulnResponse struct {
|
|
ID string `json:"id"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Severity string `json:"severity,omitempty"`
|
|
CVSSScore float64 `json:"cvss_score,omitempty"`
|
|
FixedVersion string `json:"fixed_version,omitempty"`
|
|
References []string `json:"references,omitempty"`
|
|
}
|
|
|
|
// VulnsResponse contains vulnerabilities for a package/version.
|
|
type VulnsResponse struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version,omitempty"`
|
|
Vulnerabilities []VulnResponse `json:"vulnerabilities"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// EnrichmentResponse contains full enrichment data.
|
|
type EnrichmentResponse struct {
|
|
Package *PackageResponse `json:"package,omitempty"`
|
|
Version *VersionResponse `json:"version,omitempty"`
|
|
Vulnerabilities []VulnResponse `json:"vulnerabilities,omitempty"`
|
|
IsOutdated bool `json:"is_outdated"`
|
|
LicenseCategory string `json:"license_category"`
|
|
}
|
|
|
|
// OutdatedRequest is the request body for checking outdated packages.
|
|
type OutdatedRequest struct {
|
|
Packages []OutdatedPackage `json:"packages"`
|
|
}
|
|
|
|
// OutdatedPackage represents a package to check for outdatedness.
|
|
type OutdatedPackage struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// OutdatedResponse contains outdated check results.
|
|
type OutdatedResponse struct {
|
|
Results []OutdatedResult `json:"results"`
|
|
}
|
|
|
|
// OutdatedResult contains the outdated status for a package.
|
|
type OutdatedResult struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
LatestVersion string `json:"latest_version,omitempty"`
|
|
IsOutdated bool `json:"is_outdated"`
|
|
}
|
|
|
|
// BulkRequest is the request body for bulk package lookups.
|
|
type BulkRequest struct {
|
|
PURLs []string `json:"purls"`
|
|
}
|
|
|
|
// BulkResponse contains bulk lookup results.
|
|
type BulkResponse struct {
|
|
Packages map[string]*PackageResponse `json:"packages"`
|
|
}
|
|
|
|
// HandlePackagePath dispatches /api/package/{ecosystem}/* to the appropriate handler.
|
|
// Resolves namespaced package names (Composer vendor/name, npm @scope/name) from the path.
|
|
func (h *APIHandler) HandlePackagePath(w http.ResponseWriter, r *http.Request) {
|
|
ecosystem := chi.URLParam(r, "ecosystem")
|
|
wildcard := chi.URLParam(r, "*")
|
|
segments := splitWildcardPath(wildcard)
|
|
|
|
if ecosystem == "" || len(segments) == 0 {
|
|
http.Error(w, "ecosystem and name are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// For the API, we don't have a DB to resolve names, so we use a heuristic:
|
|
// the last segment that looks like a version (contains a digit) is the version,
|
|
// everything before it is the name. If no version-like segment, it's all name.
|
|
//
|
|
// With 1 segment: package lookup (name only)
|
|
// With 2+ segments: last segment is version, rest is name
|
|
// Exception: if this is a namespaced ecosystem and we have exactly 2 segments,
|
|
// it could be vendor/name with no version. The enrichment service handles
|
|
// both cases (it will try to look up the package either way).
|
|
if len(segments) == 1 {
|
|
h.getPackage(w, r, ecosystem, segments[0])
|
|
return
|
|
}
|
|
|
|
// Try the full path as a package name first via enrichment.
|
|
// If it resolves, this is a package-only lookup.
|
|
fullName := strings.Join(segments, "/")
|
|
info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, fullName)
|
|
if err == nil && info != nil {
|
|
resp := &PackageResponse{
|
|
Ecosystem: info.Ecosystem,
|
|
Name: info.Name,
|
|
LatestVersion: info.LatestVersion,
|
|
License: info.License,
|
|
LicenseCategory: string(h.enrichment.CategorizeLicense(info.License)),
|
|
Description: info.Description,
|
|
Homepage: info.Homepage,
|
|
Repository: info.Repository,
|
|
RegistryURL: info.RegistryURL,
|
|
}
|
|
writeJSON(w, resp)
|
|
return
|
|
}
|
|
|
|
// Otherwise, last segment is the version.
|
|
name := strings.Join(segments[:len(segments)-1], "/")
|
|
version := segments[len(segments)-1]
|
|
h.getVersion(w, r, ecosystem, name, version)
|
|
}
|
|
|
|
func (h *APIHandler) getPackage(w http.ResponseWriter, r *http.Request, ecosystem, name string) {
|
|
info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, name)
|
|
if err != nil {
|
|
http.Error(w, "failed to enrich package", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if info == nil {
|
|
http.Error(w, "package not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
resp := &PackageResponse{
|
|
Ecosystem: info.Ecosystem,
|
|
Name: info.Name,
|
|
LatestVersion: info.LatestVersion,
|
|
License: info.License,
|
|
LicenseCategory: string(h.enrichment.CategorizeLicense(info.License)),
|
|
Description: info.Description,
|
|
Homepage: info.Homepage,
|
|
Repository: info.Repository,
|
|
RegistryURL: info.RegistryURL,
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
func (h *APIHandler) getVersion(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) {
|
|
result, err := h.enrichment.EnrichFull(r.Context(), ecosystem, name, version)
|
|
if err != nil {
|
|
http.Error(w, "failed to enrich version", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := &EnrichmentResponse{
|
|
IsOutdated: result.IsOutdated,
|
|
LicenseCategory: string(result.LicenseCategory),
|
|
}
|
|
|
|
if result.Package != nil {
|
|
resp.Package = &PackageResponse{
|
|
Ecosystem: result.Package.Ecosystem,
|
|
Name: result.Package.Name,
|
|
LatestVersion: result.Package.LatestVersion,
|
|
License: result.Package.License,
|
|
LicenseCategory: string(h.enrichment.CategorizeLicense(result.Package.License)),
|
|
Description: result.Package.Description,
|
|
Homepage: result.Package.Homepage,
|
|
Repository: result.Package.Repository,
|
|
RegistryURL: result.Package.RegistryURL,
|
|
}
|
|
}
|
|
|
|
if result.Version != nil {
|
|
resp.Version = &VersionResponse{
|
|
Ecosystem: ecosystem,
|
|
Name: name,
|
|
Version: result.Version.Number,
|
|
License: result.Version.License,
|
|
Integrity: result.Version.Integrity,
|
|
Yanked: result.Version.Yanked,
|
|
IsOutdated: result.IsOutdated,
|
|
}
|
|
if !result.Version.PublishedAt.IsZero() {
|
|
resp.Version.PublishedAt = result.Version.PublishedAt.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
}
|
|
|
|
for _, v := range result.Vulnerabilities {
|
|
resp.Vulnerabilities = append(resp.Vulnerabilities, VulnResponse{
|
|
ID: v.ID,
|
|
Summary: v.Summary,
|
|
Severity: v.Severity,
|
|
CVSSScore: v.CVSSScore,
|
|
FixedVersion: v.FixedVersion,
|
|
References: v.References,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// HandleVulnsPath dispatches /api/vulns/{ecosystem}/* to the vulns handler.
|
|
// Supports both {name} and {name}/{version} paths with namespaced package names.
|
|
func (h *APIHandler) HandleVulnsPath(w http.ResponseWriter, r *http.Request) {
|
|
ecosystem := chi.URLParam(r, "ecosystem")
|
|
wildcard := chi.URLParam(r, "*")
|
|
segments := splitWildcardPath(wildcard)
|
|
|
|
if ecosystem == "" || len(segments) == 0 {
|
|
http.Error(w, "ecosystem and name are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Last segment could be a version. Try full path as name first,
|
|
// then split off the last segment as version.
|
|
name := strings.Join(segments, "/")
|
|
version := "0"
|
|
|
|
if len(segments) > 1 {
|
|
// Try enrichment with the full path as name.
|
|
// If it doesn't resolve, assume last segment is version.
|
|
info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, name)
|
|
if err != nil || info == nil {
|
|
name = strings.Join(segments[:len(segments)-1], "/")
|
|
version = segments[len(segments)-1]
|
|
}
|
|
}
|
|
|
|
vulns, err := h.enrichment.CheckVulnerabilities(r.Context(), ecosystem, name, version)
|
|
if err != nil {
|
|
http.Error(w, "failed to check vulnerabilities", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := &VulnsResponse{
|
|
Ecosystem: ecosystem,
|
|
Name: name,
|
|
Version: version,
|
|
Count: len(vulns),
|
|
}
|
|
|
|
for _, v := range vulns {
|
|
resp.Vulnerabilities = append(resp.Vulnerabilities, VulnResponse{
|
|
ID: v.ID,
|
|
Summary: v.Summary,
|
|
Severity: v.Severity,
|
|
CVSSScore: v.CVSSScore,
|
|
FixedVersion: v.FixedVersion,
|
|
References: v.References,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// HandleOutdated handles POST /api/outdated
|
|
// @Summary Check outdated packages
|
|
// @Tags api
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body OutdatedRequest true "Packages to check"
|
|
// @Success 200 {object} OutdatedResponse
|
|
// @Failure 400 {string} string
|
|
// @Failure 500 {string} string
|
|
// @Router /api/outdated [post]
|
|
func (h *APIHandler) HandleOutdated(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
|
var req OutdatedRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req.Packages) == 0 {
|
|
http.Error(w, "packages list is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := OutdatedResponse{
|
|
Results: make([]OutdatedResult, 0, len(req.Packages)),
|
|
}
|
|
|
|
for _, pkg := range req.Packages {
|
|
result := OutdatedResult{
|
|
Ecosystem: pkg.Ecosystem,
|
|
Name: pkg.Name,
|
|
Version: pkg.Version,
|
|
}
|
|
|
|
latest, err := h.enrichment.GetLatestVersion(r.Context(), pkg.Ecosystem, pkg.Name)
|
|
if err == nil && latest != "" {
|
|
result.LatestVersion = latest
|
|
result.IsOutdated = h.enrichment.IsOutdated(pkg.Version, latest)
|
|
}
|
|
|
|
resp.Results = append(resp.Results, result)
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// HandleBulkLookup handles POST /api/bulk
|
|
// @Summary Bulk package lookup by PURL
|
|
// @Tags api
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body BulkRequest true "PURLs"
|
|
// @Success 200 {object} BulkResponse
|
|
// @Failure 400 {string} string
|
|
// @Failure 500 {string} string
|
|
// @Router /api/bulk [post]
|
|
func (h *APIHandler) HandleBulkLookup(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
|
var req BulkRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req.PURLs) == 0 {
|
|
http.Error(w, "purls list is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := BulkResponse{
|
|
Packages: make(map[string]*PackageResponse),
|
|
}
|
|
|
|
// Use ecosystems client for bulk lookup if available
|
|
if h.ecosystems != nil {
|
|
packages, err := h.ecosystems.BulkLookup(r.Context(), req.PURLs)
|
|
if err == nil {
|
|
for purl, info := range packages {
|
|
if info != nil {
|
|
resp.Packages[purl] = &PackageResponse{
|
|
Ecosystem: info.Ecosystem,
|
|
Name: info.Name,
|
|
LatestVersion: info.LatestVersion,
|
|
License: info.License,
|
|
LicenseCategory: string(h.enrichment.CategorizeLicense(info.License)),
|
|
Description: info.Description,
|
|
Homepage: info.Homepage,
|
|
Repository: info.Repository,
|
|
RegistryURL: info.RegistryURL,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Fall back to individual lookups via registries
|
|
packages := make([]struct{ Ecosystem, Name string }, 0, len(req.PURLs))
|
|
|
|
for _, purlStr := range req.PURLs {
|
|
p, err := purl.Parse(purlStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ecosystem := purl.PURLTypeToEcosystem(p.Type)
|
|
name := p.FullName()
|
|
packages = append(packages, struct{ Ecosystem, Name string }{ecosystem, name})
|
|
}
|
|
|
|
results := h.enrichment.BulkEnrichPackages(r.Context(), packages)
|
|
for purlStr, info := range results {
|
|
if info != nil {
|
|
resp.Packages[purlStr] = &PackageResponse{
|
|
Ecosystem: info.Ecosystem,
|
|
Name: info.Name,
|
|
LatestVersion: info.LatestVersion,
|
|
License: info.License,
|
|
LicenseCategory: string(h.enrichment.CategorizeLicense(info.License)),
|
|
Description: info.Description,
|
|
Homepage: info.Homepage,
|
|
Repository: info.Repository,
|
|
RegistryURL: info.RegistryURL,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// SearchResponse contains search results.
|
|
type SearchResponse struct {
|
|
Results []SearchPackageResult `json:"results"`
|
|
Query string `json:"query"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// SearchPackageResult represents a single search result.
|
|
type SearchPackageResult struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
LatestVersion string `json:"latest_version,omitempty"`
|
|
License string `json:"license,omitempty"`
|
|
Hits int64 `json:"hits"`
|
|
Size int64 `json:"size"`
|
|
CachedAt string `json:"cached_at,omitempty"`
|
|
}
|
|
|
|
// HandleSearch handles GET /api/search
|
|
// @Summary Search cached packages
|
|
// @Tags api
|
|
// @Produce json
|
|
// @Param q query string true "Query"
|
|
// @Param ecosystem query string false "Ecosystem"
|
|
// @Success 200 {object} SearchResponse
|
|
// @Failure 400 {string} string
|
|
// @Failure 500 {string} string
|
|
// @Router /api/search [get]
|
|
func (h *APIHandler) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("q")
|
|
ecosystem := r.URL.Query().Get("ecosystem")
|
|
|
|
if query == "" {
|
|
http.Error(w, "query parameter 'q' is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
page := 1
|
|
limit := 50
|
|
|
|
// Search in database
|
|
results, err := h.db.SearchPackages(query, ecosystem, limit, (page-1)*limit)
|
|
if err != nil {
|
|
http.Error(w, "search failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
total, err := h.db.CountSearchResults(query, ecosystem)
|
|
if err != nil {
|
|
total = 0
|
|
}
|
|
|
|
resp := &SearchResponse{
|
|
Query: query,
|
|
Count: int(total),
|
|
Results: make([]SearchPackageResult, 0, len(results)),
|
|
}
|
|
|
|
for _, result := range results {
|
|
latestVersion := ""
|
|
if result.LatestVersion.Valid {
|
|
latestVersion = result.LatestVersion.String
|
|
}
|
|
license := ""
|
|
if result.License.Valid {
|
|
license = result.License.String
|
|
}
|
|
searchResult := SearchPackageResult{
|
|
Ecosystem: result.Ecosystem,
|
|
Name: result.Name,
|
|
LatestVersion: latestVersion,
|
|
License: license,
|
|
Hits: result.Hits,
|
|
Size: result.Size,
|
|
}
|
|
if result.CachedAt.Valid {
|
|
searchResult.CachedAt = result.CachedAt.String
|
|
}
|
|
resp.Results = append(resp.Results, searchResult)
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
|
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// PackagesListResponse contains a list of cached packages.
|
|
type PackagesListResponse struct {
|
|
Results []PackageListResult `json:"results"`
|
|
Count int `json:"count"`
|
|
Total int64 `json:"total"`
|
|
Ecosystem string `json:"ecosystem,omitempty"`
|
|
SortBy string `json:"sort_by"`
|
|
Page int `json:"page"`
|
|
PerPage int `json:"per_page"`
|
|
}
|
|
|
|
// PackageListResult represents a single package in the list.
|
|
type PackageListResult struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
LatestVersion string `json:"latest_version,omitempty"`
|
|
License string `json:"license,omitempty"`
|
|
LicenseCategory string `json:"license_category,omitempty"`
|
|
Hits int64 `json:"hits"`
|
|
Size int64 `json:"size"`
|
|
CachedAt string `json:"cached_at,omitempty"`
|
|
VulnCount int64 `json:"vuln_count"`
|
|
}
|
|
|
|
// HandlePackagesList handles GET /api/packages
|
|
// @Summary List cached packages
|
|
// @Tags api
|
|
// @Produce json
|
|
// @Param ecosystem query string false "Ecosystem"
|
|
// @Param sort query string false "Sort" Enums(hits,name,size,cached_at,ecosystem,vulns)
|
|
// @Success 200 {object} PackagesListResponse
|
|
// @Failure 400 {string} string
|
|
// @Failure 500 {string} string
|
|
// @Router /api/packages [get]
|
|
func (h *APIHandler) HandlePackagesList(w http.ResponseWriter, r *http.Request) {
|
|
ecosystem := r.URL.Query().Get("ecosystem")
|
|
sortBy := r.URL.Query().Get("sort")
|
|
if sortBy == "" {
|
|
sortBy = defaultSortBy
|
|
}
|
|
|
|
validSorts := map[string]bool{
|
|
defaultSortBy: true,
|
|
"name": true,
|
|
"size": true,
|
|
"cached_at": true,
|
|
"ecosystem": true,
|
|
"vulns": true,
|
|
}
|
|
if !validSorts[sortBy] {
|
|
http.Error(w, "invalid sort parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
page := 1
|
|
limit := 50
|
|
|
|
packages, err := h.db.ListCachedPackages(ecosystem, sortBy, limit, (page-1)*limit)
|
|
if err != nil {
|
|
http.Error(w, "failed to list packages", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
total, err := h.db.CountCachedPackages(ecosystem)
|
|
if err != nil {
|
|
total = 0
|
|
}
|
|
|
|
resp := &PackagesListResponse{
|
|
Results: make([]PackageListResult, 0, len(packages)),
|
|
Count: len(packages),
|
|
Total: total,
|
|
Ecosystem: ecosystem,
|
|
SortBy: sortBy,
|
|
Page: page,
|
|
PerPage: limit,
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
latestVersion := ""
|
|
if pkg.LatestVersion.Valid {
|
|
latestVersion = pkg.LatestVersion.String
|
|
}
|
|
license := ""
|
|
licenseCategory := licenseCategoryUnknown
|
|
if pkg.License.Valid {
|
|
license = pkg.License.String
|
|
if h.enrichment != nil {
|
|
licenseCategory = string(h.enrichment.CategorizeLicense(license))
|
|
}
|
|
}
|
|
cachedAt := ""
|
|
if pkg.CachedAt.Valid {
|
|
cachedAt = pkg.CachedAt.String
|
|
}
|
|
|
|
resp.Results = append(resp.Results, PackageListResult{
|
|
Ecosystem: pkg.Ecosystem,
|
|
Name: pkg.Name,
|
|
LatestVersion: latestVersion,
|
|
License: license,
|
|
LicenseCategory: licenseCategory,
|
|
Hits: pkg.Hits,
|
|
Size: pkg.Size,
|
|
CachedAt: cachedAt,
|
|
VulnCount: pkg.VulnCount,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, resp)
|
|
}
|