Skip to content
346 changes: 337 additions & 9 deletions controllers/project_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"

cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/google/uuid"
"github.com/l3montree-dev/devguard/database/models"
"github.com/l3montree-dev/devguard/dtos"
"github.com/l3montree-dev/devguard/normalize"
"github.com/l3montree-dev/devguard/shared"
"github.com/l3montree-dev/devguard/transformer"
"github.com/l3montree-dev/devguard/utils"
Expand All @@ -30,18 +34,30 @@ import (
)

type ProjectController struct {
projectRepository shared.ProjectRepository
assetRepository shared.AssetRepository
projectService shared.ProjectService
webhookRepository shared.WebhookIntegrationRepository
projectRepository shared.ProjectRepository
assetRepository shared.AssetRepository
assetVersionRepository shared.AssetVersionRepository
artifactRepository shared.ArtifactRepository
assetVersionService shared.AssetVersionService
assetService shared.AssetService
releaseService shared.ReleaseService
projectService shared.ProjectService
webhookRepository shared.WebhookIntegrationRepository
scanService shared.ScanService
}

func NewProjectController(repository shared.ProjectRepository, assetRepository shared.AssetRepository, projectService shared.ProjectService, webhookRepository shared.WebhookIntegrationRepository) *ProjectController {
func NewProjectController(repository shared.ProjectRepository, assetRepository shared.AssetRepository, assetVersionRepository shared.AssetVersionRepository, artifactRepository shared.ArtifactRepository, assetVersionService shared.AssetVersionService, assetService shared.AssetService, releaseService shared.ReleaseService, projectService shared.ProjectService, webhookRepository shared.WebhookIntegrationRepository, scanService shared.ScanService) *ProjectController {
return &ProjectController{
projectRepository: repository,
assetRepository: assetRepository,
projectService: projectService,
webhookRepository: webhookRepository,
projectRepository: repository,
assetRepository: assetRepository,
assetVersionRepository: assetVersionRepository,
artifactRepository: artifactRepository,
assetVersionService: assetVersionService,
assetService: assetService,
releaseService: releaseService,
projectService: projectService,
webhookRepository: webhookRepository,
scanService: scanService,
}
}

Expand Down Expand Up @@ -517,3 +533,315 @@ func (ProjectController *ProjectController) UpdateConfigFile(ctx shared.Context)
}
return ctx.String(200, configContent)
}

func (ProjectController *ProjectController) HandleDynamicProject(ctx shared.Context) error {

body, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("could not read request body: %s", err.Error())).WithInternal(err)
}

var probe dtos.DynamicProjectRequestDTO
if err := json.Unmarshal(body, &probe); err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("could not parse request body: %s", err.Error())).WithInternal(err)
}

if probe.ProjectExternalEntityID == "" || probe.AssetExternalEntityID == "" {
return echo.NewHTTPError(400, "verb, projectExternalEntityId, and assetExternalEntityId are required")
}

action := probe.Verb
projectName := probe.ProjectName
projectExternalEntityID := probe.ProjectExternalEntityID
projectDescription := probe.ProjectDescription

subProjectExternalEntityID := probe.SubProjectExternalEntityID
subProjectName := probe.SubProjectName
subProjectDescription := probe.SubProjectDescription

assetName := probe.AssetName
assetExternalEntityID := probe.AssetExternalEntityID
assetDescription := probe.AssetDescription

assetVersionName := probe.AssetVersionName
artifactName := probe.Artifact

providerID := shared.GetProviderID(ctx)
organization := shared.GetOrg(ctx)
parentProject := shared.GetProject(ctx)
userID := shared.GetSession(ctx).GetUserID()

if action == "delete" {
parentProjectID := parentProject.ID
proExternalEntityID := projectExternalEntityID
if subProjectExternalEntityID != "" {
subProjectParent, err := ProjectController.projectRepository.GetDirectChildProjectsWithProviderIDAndExternalEntityID(ctx.Request().Context(), nil, parentProject.ID, providerID, projectExternalEntityID)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not fetch sub-projects: %s", err.Error())).WithInternal(err)
}
parentProjectID = subProjectParent.ID
proExternalEntityID = subProjectExternalEntityID
}
err := ProjectController.projectRepository.CleanupDynamicProject(ctx.Request().Context(), nil, organization.GetID(), parentProjectID, providerID, proExternalEntityID, assetExternalEntityID, assetVersionName, artifactName)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not delete project: %s", err.Error())).WithInternal(err)
}

return ctx.JSON(200, map[string]string{"message": "project and asset deleted successfully"})
} else if action != "update" {
return echo.NewHTTPError(400, "invalid verb, only 'update' and 'delete' are allowed")
}

bom := new(cdx.BOM)

if probe.Sbom == nil {
return echo.NewHTTPError(400, "sbom is required")
}
if err := json.Unmarshal(probe.Sbom, bom); err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("could not parse CycloneDX BOM: %s", err.Error())).WithInternal(err)
}

project, err := ProjectController.projectService.FindOrCreateProject(ctx, providerID, organization.GetID(), projectName, projectExternalEntityID, parentProject.ID, projectDescription)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not create project: %s", err.Error())).WithInternal(err)
}

pID := project.ID

if subProjectExternalEntityID != "" {
subProject, err := ProjectController.projectService.FindOrCreateProject(ctx, providerID, organization.GetID(), subProjectName, subProjectExternalEntityID, project.ID, subProjectDescription)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not create sub-project: %s", err.Error())).WithInternal(err)
}
pID = subProject.ID
}

rbac := shared.GetRBAC(ctx)
asset, err := ProjectController.assetService.FindOrCreateAsset(ctx.Request().Context(), rbac, providerID, organization.GetID(), pID, assetName, assetExternalEntityID, userID, assetDescription)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not create asset: %s", err.Error())).WithInternal(err)
}

assetVersion, err := ProjectController.assetVersionRepository.FindOrCreate(ctx.Request().Context(), nil, assetVersionName, asset.ID, false, nil)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not create asset version: %s", err.Error())).WithInternal(err)
}

artifact := models.Artifact{
ArtifactName: artifactName,
AssetVersionName: assetVersion.Name,
AssetID: asset.ID,
}
if err := ProjectController.artifactRepository.Save(ctx.Request().Context(), nil, &artifact); err != nil {
slog.Error("trivy operator: could not save artifact", "err", err)
return ctx.JSON(500, map[string]string{"error": "could not save artifact"})
}

release, err := ProjectController.releaseService.FindOrCreate(ctx.Request().Context(), parentProject.ID, providerID)
if err != nil {
return echo.NewHTTPError(500, fmt.Sprintf("could not create release: %s", err.Error())).WithInternal(err)
}

//add or update release item
releaseItem := models.ReleaseItem{
ReleaseID: release.ID,
ArtifactName: &artifact.ArtifactName,
AssetID: &asset.ID,
AssetVersionName: &assetVersion.Name,
}

err = ProjectController.releaseService.AddItem(ctx.Request().Context(), &releaseItem)
if err != nil {
slog.Error("could not add release item", "err", err)
return ctx.JSON(500, map[string]string{"error": "could not add release item"})
}

normalized, err := normalize.SBOMGraphFromCycloneDX(bom, artifactName, "operator", asset.KeepOriginalSbomRootComponent)
if err != nil {
slog.Error("trivy operator: failed to normalize BOM", "err", err)
return ctx.JSON(400, map[string]string{"error": "could not normalize SBOM"})
}

wholeSBOM, err := ProjectController.assetVersionService.UpdateSBOM(ctx.Request().Context(), nil, organization, *project, *asset, assetVersion, artifactName, normalized)
if err != nil {
slog.Error("trivy operator: could not update SBOM", "err", err)
return ctx.JSON(500, map[string]string{"error": "could not update SBOM"})
}

tx := ProjectController.artifactRepository.GetDB(ctx.Request().Context(), nil).Begin()
defer tx.Rollback()

userAgent := ctx.Request().UserAgent()
_, _, _, err = ProjectController.scanService.ScanNormalizedSBOM(ctx.Request().Context(), tx, organization, *project, *asset, assetVersion, artifact, wholeSBOM, userID, &userAgent)
if err != nil {
slog.Error("trivy operator: scan failed", "err", err)
return ctx.JSON(500, map[string]string{"error": "scan failed"})
}

tx.Commit()
return ctx.JSON(200, map[string]string{"message": "project and asset created, SBOM processed and scan started successfully"})
}

func (ProjectController *ProjectController) ListDynamicProjects(ctx shared.Context) error {
reqCtx := ctx.Request().Context()
parentProject := shared.GetProject(ctx)
providerID := shared.GetProviderID(ctx)

// Query 1: direct child projects filtered by providerID
projects, err := ProjectController.projectRepository.GetDirectChildProjectsWithProviderID(reqCtx, nil, parentProject.ID, providerID)
if err != nil {
return echo.NewHTTPError(500, "could not list projects").WithInternal(err)
}
if len(projects) == 0 {
return ctx.JSON(200, []dtos.ProjectsAssetAssetVersionsDTO{})
}

projectIDs := make([]uuid.UUID, len(projects))
for i, p := range projects {
projectIDs[i] = p.ID
}

// Query 2: all sub-projects for all parent projects in one shot
subProjects, err := ProjectController.projectRepository.GetChildProjectsForParents(reqCtx, nil, projectIDs, providerID)
if err != nil {
return echo.NewHTTPError(500, "could not list sub-projects").WithInternal(err)
}

subProjectIDs := make([]uuid.UUID, len(subProjects))
for i, sp := range subProjects {
subProjectIDs[i] = sp.ID
}

// Query 3: all assets for projects + sub-projects, filtered by providerID
allProjectIDs := append(projectIDs, subProjectIDs...)
allAssets, err := ProjectController.assetRepository.GetByProjectIDsWithProviderID(reqCtx, nil, allProjectIDs, providerID)
if err != nil {
return echo.NewHTTPError(500, "could not list assets").WithInternal(err)
}

allAssetIDs := make([]uuid.UUID, len(allAssets))
for i, a := range allAssets {
allAssetIDs[i] = a.ID
}

// Query 4: all asset versions for all assets
allAssetVersions, err := ProjectController.assetVersionRepository.GetAssetVersionsByAssetIDs(reqCtx, nil, allAssetIDs)
if err != nil {
return echo.NewHTTPError(500, "could not list asset versions").WithInternal(err)
}

// Query 5: all artifacts for all assets
allArtifacts, err := ProjectController.artifactRepository.GetByAssetIDs(reqCtx, nil, allAssetIDs)
if err != nil {
return echo.NewHTTPError(500, "could not list artifacts").WithInternal(err)
}

// Build in-memory lookup maps
subProjectsByParentID := make(map[uuid.UUID][]models.Project)
for _, sp := range subProjects {
if sp.ParentID != nil {
subProjectsByParentID[*sp.ParentID] = append(subProjectsByParentID[*sp.ParentID], sp)
}
}

assetsByProjectID := make(map[uuid.UUID][]models.Asset)
for _, a := range allAssets {
assetsByProjectID[a.ProjectID] = append(assetsByProjectID[a.ProjectID], a)
}

versionsByAssetID := make(map[uuid.UUID][]models.AssetVersion)
for _, av := range allAssetVersions {
versionsByAssetID[av.AssetID] = append(versionsByAssetID[av.AssetID], av)
}

artifactsByAssetIDAndVersion := make(map[uuid.UUID]map[string][]string)
for _, art := range allArtifacts {
if artifactsByAssetIDAndVersion[art.AssetID] == nil {
artifactsByAssetIDAndVersion[art.AssetID] = make(map[string][]string)
}
artifactsByAssetIDAndVersion[art.AssetID][art.AssetVersionName] = append(
artifactsByAssetIDAndVersion[art.AssetID][art.AssetVersionName], art.ArtifactName,
)
}

buildAssetEntries := func(projectID uuid.UUID) []struct {
AssetExternalEntityID string `json:"assetExternalEntityId"`
AssetName string `json:"assetName"`
AssetVersions []struct {
AssetVersionName string `json:"assetVersionName"`
Artifacts []string `json:"artifacts"`
} `json:"assetVersions"`
} {
var entries []struct {
AssetExternalEntityID string `json:"assetExternalEntityId"`
AssetName string `json:"assetName"`
AssetVersions []struct {
AssetVersionName string `json:"assetVersionName"`
Artifacts []string `json:"artifacts"`
} `json:"assetVersions"`
}
for _, asset := range assetsByProjectID[projectID] {
if asset.ExternalEntityID == nil {
continue
}
versions := versionsByAssetID[asset.ID]
if len(versions) == 0 {
continue
}
assetEntry := struct {
AssetExternalEntityID string `json:"assetExternalEntityId"`
AssetName string `json:"assetName"`
AssetVersions []struct {
AssetVersionName string `json:"assetVersionName"`
Artifacts []string `json:"artifacts"`
} `json:"assetVersions"`
}{AssetExternalEntityID: *asset.ExternalEntityID, AssetName: asset.Name}
for _, av := range versions {
avEntry := struct {
AssetVersionName string `json:"assetVersionName"`
Artifacts []string `json:"artifacts"`
}{AssetVersionName: av.Name, Artifacts: artifactsByAssetIDAndVersion[asset.ID][av.Name]}
assetEntry.AssetVersions = append(assetEntry.AssetVersions, avEntry)
}
entries = append(entries, assetEntry)
}
return entries
}

result := make([]dtos.ProjectsAssetAssetVersionsDTO, 0, len(projects))
for _, project := range projects {
if project.ExternalEntityID == nil {
continue
}
entry := dtos.ProjectsAssetAssetVersionsDTO{
ProjectExternalEntityID: *project.ExternalEntityID,
ProjectName: project.Name,
}

for _, sp := range subProjectsByParentID[project.ID] {
if sp.ExternalEntityID == nil {
continue
}
spEntry := struct {
SubProjectExternalEntityID string `json:"subProjectExternalEntityId,omitempty"`
SubProjectName string `json:"subProjectName,omitempty"`
SubProjectDescription string `json:"subProjectDescription,omitempty"`
Assets []struct {
AssetExternalEntityID string `json:"assetExternalEntityId"`
AssetName string `json:"assetName"`
AssetVersions []struct {
AssetVersionName string `json:"assetVersionName"`
Artifacts []string `json:"artifacts"`
} `json:"assetVersions"`
} `json:"assets"`
}{SubProjectExternalEntityID: *sp.ExternalEntityID, SubProjectName: sp.Name}
spEntry.Assets = buildAssetEntries(sp.ID)
entry.SubProjects = append(entry.SubProjects, spEntry)
}

entry.Assets = buildAssetEntries(project.ID)
result = append(result, entry)
}

return ctx.JSON(200, result)
}
5 changes: 2 additions & 3 deletions database/models/project_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import (
type ProjectType string

const (
ProjectTypeDefault ProjectType = "default"
ProjectTypeKubernetesNamespace ProjectType = "kubernetesNamespace"
ProjectTypeKubernetesCluster ProjectType = "kubernetesCluster"
ProjectTypeDefault ProjectType = "default"
ProjectTypeDynamic ProjectType = "dynamic"
)

type ProjectState string
Expand Down
Loading
Loading