package handlers import ( "net/http" "strconv" "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/domain/sbom" ) type SBOMHandler struct { db *xorm.Engine generator *sbom.Generator } func NewSBOMHandler(db *xorm.Engine, gen *sbom.Generator) *SBOMHandler { return &SBOMHandler{db: db, generator: gen} } // GetForRun returns the SBOM report metadata for a pipeline run. // GET /api/v1/repos/{owner}/{repo}/runs/{runID}/sbom func (h *SBOMHandler) GetForRun(w http.ResponseWriter, r *http.Request) { runID, err := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64) if err != nil { jsonError(w, "invalid run ID", http.StatusBadRequest) return } report, err := h.generator.GetForRun(runID) if err != nil { jsonError(w, "database error", http.StatusInternalServerError) return } if report == nil { jsonError(w, "SBOM not yet generated for this run", http.StatusNotFound) return } jsonOK(w, report) } // GetDocumentForRun streams the full CycloneDX JSON document for a run. // GET /api/v1/repos/{owner}/{repo}/runs/{runID}/sbom/document func (h *SBOMHandler) GetDocumentForRun(w http.ResponseWriter, r *http.Request) { runID, err := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64) if err != nil { jsonError(w, "invalid run ID", http.StatusBadRequest) return } report, err := h.generator.GetForRun(runID) if err != nil { jsonError(w, "database error", http.StatusInternalServerError) return } if report == nil { jsonError(w, "SBOM not yet generated for this run", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/vnd.cyclonedx+json") w.Header().Set("Content-Disposition", `attachment; filename="bom.json"`) w.WriteHeader(http.StatusOK) w.Write([]byte(report.BOMDocument)) //nolint:errcheck } // GetLatest returns the most recent SBOM report metadata for a repo. // GET /api/v1/repos/{owner}/{repo}/sbom func (h *SBOMHandler) GetLatest(w http.ResponseWriter, r *http.Request) { repoID, ok := resolveRepoID(h.db, w, r) if !ok { return } report, err := h.generator.GetLatest(repoID) if err != nil { jsonError(w, "database error", http.StatusInternalServerError) return } if report == nil { jsonError(w, "no SBOM generated yet — push a commit or trigger a pipeline run", http.StatusNotFound) return } jsonOK(w, report) } // GetLatestDocument streams the latest CycloneDX JSON for a repo. // GET /api/v1/repos/{owner}/{repo}/sbom/document func (h *SBOMHandler) GetLatestDocument(w http.ResponseWriter, r *http.Request) { repoID, ok := resolveRepoID(h.db, w, r) if !ok { return } report, err := h.generator.GetLatest(repoID) if err != nil { jsonError(w, "database error", http.StatusInternalServerError) return } if report == nil { jsonError(w, "no SBOM generated yet", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/vnd.cyclonedx+json") w.Header().Set("Content-Disposition", `attachment; filename="bom.json"`) w.WriteHeader(http.StatusOK) w.Write([]byte(report.BOMDocument)) //nolint:errcheck } // Generate triggers on-demand SBOM generation for a repo at a given ref/SHA. // POST /api/v1/repos/{owner}/{repo}/sbom/generate?ref= func (h *SBOMHandler) Generate(w http.ResponseWriter, r *http.Request) { repoID, ok := resolveRepoID(h.db, w, r) if !ok { return } sha := r.URL.Query().Get("ref") if sha == "" { sha = r.URL.Query().Get("sha") } if sha == "" { jsonError(w, "ref or sha query param required", http.StatusBadRequest) return } report, err := h.generator.GenerateOnDemand(repoID, sha) if err != nil { jsonError(w, "generation failed: "+err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) jsonOK(w, report) }