Skip to content
Draft
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
1 change: 1 addition & 0 deletions cmd/devguard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func main() {
fx.Invoke(func(FalsePositiveRuleRouter router.VEXRuleRouter) {}),
fx.Invoke(func(ExternalReferenceRouter router.ExternalReferenceRouter) {}),
fx.Invoke(func(CrowdsourcedVexingRouter router.CrowdsourcedVexingRouter) {}),
fx.Invoke(func(AdvisoryRouter router.AdvisoryRouter) {}),
fx.Invoke(func(lc fx.Lifecycle, encryptionService shared.DBEncryptionService) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
Expand Down
120 changes: 120 additions & 0 deletions controllers/advisory_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (C) 2023 Tim Bastin, l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package controllers

import (
"github.com/google/uuid"
"github.com/l3montree-dev/devguard/dtos"
"github.com/l3montree-dev/devguard/shared"
"github.com/l3montree-dev/devguard/transformer"
"github.com/labstack/echo/v4"
)

type AdvisoryController struct {
advisoryService shared.AdvisoryService
}

func NewAdvisoryController(advisoryService shared.AdvisoryService) *AdvisoryController {
return &AdvisoryController{
advisoryService: advisoryService,
}
}

func (controller *AdvisoryController) Create(ctx shared.Context) error {
var req dtos.AdvisoryCreate
if err := ctx.Bind(&req); err != nil {
return echo.NewHTTPError(400, "unable to process request").WithInternal(err)
}

newAdvisory := transformer.AdvisoryCreateRequestToModel(req)

err := controller.advisoryService.Create(ctx.Request().Context(), &newAdvisory)

if err != nil {
return echo.NewHTTPError(409, "could not set advisory").WithInternal(err)
}

return ctx.NoContent(200)
}

func (controller *AdvisoryController) ReadAll(ctx shared.Context) error {
asset := shared.GetAsset(ctx)
advisories, err := controller.advisoryService.ReadAll(ctx.Request().Context(), asset.ID)
if err != nil {
return echo.NewHTTPError(500, "could not get any data").WithInternal(err)
}
return ctx.JSON(200, advisories)
}

func (controller *AdvisoryController) ReadAdvisory(ctx shared.Context) error {
advisoryID := ctx.Param("id")
parsedID, err := uuid.Parse(advisoryID)
if err != nil {
return echo.NewHTTPError(400, "invalid uuid provided")
}

advisory, err := controller.advisoryService.ReadAdvisory(ctx.Request().Context(), parsedID)

if err != nil {
return echo.NewHTTPError(409, "could not get any data").WithInternal(err)
}

return ctx.JSON(200, advisory)
}

func (controller *AdvisoryController) Update(ctx shared.Context) error {
var req dtos.AdvisoryUpdate
if err := ctx.Bind(&req); err != nil {
return echo.NewHTTPError(400, "unable to process request").WithInternal(err)
}

advisoryID := ctx.Param("id")
parsedID, err := uuid.Parse(advisoryID)
if err != nil {
return echo.NewHTTPError(400, "invalid uuid provided")
}

advisory, err := controller.advisoryService.ReadAdvisory(ctx.Request().Context(), parsedID)
if err != nil {
return echo.NewHTTPError(404, "advisory not found").WithInternal(err)
}

advisory = transformer.AdvisoryUpdateRequestToModel(req, advisory)

err = controller.advisoryService.Update(ctx.Request().Context(), parsedID, &advisory)

if err != nil {
return echo.NewHTTPError(409, "could not update advisory").WithInternal(err)
}

return ctx.NoContent(200)
}

func (controller *AdvisoryController) Delete(ctx shared.Context) error {
advisoryID := ctx.Param("id")
parsedID, err := uuid.Parse(advisoryID)
if err != nil {
return echo.NewHTTPError(400, "invalid uuid provided")
}

err = controller.advisoryService.Delete(ctx.Request().Context(), parsedID)

if err != nil {
return echo.NewHTTPError(409, "could not remove name").WithInternal(err)
}

return ctx.NoContent(200)
}
2 changes: 2 additions & 0 deletions controllers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,6 @@ var ControllerModule = fx.Options(

//Crowdsourced Vexing
fx.Provide(NewCrowdsourcedVexingController),

fx.Provide(NewAdvisoryController),
)
27 changes: 27 additions & 0 deletions database/migrations/20260623101120_add_advisory_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE TABLE public.advisories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
title TEXT NOT NULL,
description TEXT NOT NULL,
severity TEXT,
vector_string TEXT,
asset_id UUID
);

CREATE TABLE public.affected_packages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
ecosystem TEXT NOT NULL,
package_name TEXT,
semver_introduced public.semver,
semver_fixed public.semver
);

CREATE TABLE public.advisories_affected_packages (
advisory_id UUID NOT NULL REFERENCES public.advisories(id) ON DELETE CASCADE,
affected_package_id UUID NOT NULL REFERENCES public.affected_packages(id) ON DELETE CASCADE,
CONSTRAINT advisories_affected_packages_pkey PRIMARY KEY (advisory_id, affected_package_id)
);

29 changes: 29 additions & 0 deletions database/models/advisory_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package models

import "github.com/google/uuid"

type Advisory struct {
Model
Title string `json:"title" gorm:"type:text;column:title"`
Description string `json:"description" gorm:"type:text;column:description"`
AffectedPackages []AffectedPackage `json:"affectedPackages" gorm:"many2many:advisories_affected_packages;foreignKey:ID;joinForeignKey:advisory_id;References:ID;joinReferences:affected_package_id;constraint:OnDelete:CASCADE"`
Severity string `json:"severity" gorm:"type:text;column:severity"`
VectorString string `json:"vectorstring" gorm:"type:text;column:vector_string"`
AssetID uuid.UUID `json:"assetID" gorm:"type:uuid;column:asset_id"`
}
type AffectedPackage struct {
Model
Ecosystem string `json:"ecosystem" gorm:"type:text;column:ecosystem"`
PackageName string `json:"packagename" gorm:"type:text;column:package_name"`
SemverIntroduced *string `json:"semverStart" gorm:"type:semver;index"`
SemverFixed *string `json:"semverEnd" gorm:"type:semver;index"`
Advisory []Advisory `json:"advisory" gorm:"many2many:advisories_affected_packages;constraint:OnDelete:CASCADE"`
}

func (m Advisory) TableName() string {
return "advisories"
}

func (m AffectedPackage) TableName() string {
return "affected_packages"
}
69 changes: 69 additions & 0 deletions database/repositories/advisory_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package repositories

import (
"context"

"github.com/google/uuid"
"github.com/l3montree-dev/devguard/database/models"
"github.com/l3montree-dev/devguard/shared"
"github.com/l3montree-dev/devguard/utils"
"gorm.io/gorm"
)

type AdvisoryRepository struct {
db *gorm.DB
utils.Repository[uuid.UUID, models.Advisory, *gorm.DB]
}

func NewAdvisoryRepository(db *gorm.DB) *AdvisoryRepository {
return &AdvisoryRepository{
db: db,
Repository: newGormRepository[uuid.UUID, models.Advisory](db),
}
}

var _ shared.AdvisoryRepository = (*AdvisoryRepository)(nil)

func (advisoryRepository *AdvisoryRepository) Create(ctx context.Context, tx *gorm.DB, advisory *models.Advisory) error {
err := advisoryRepository.GetDB(ctx, tx).Create(advisory).Error
if err != nil {
return err
}
return nil
}

func (advisoryRepository *AdvisoryRepository) ReadAll(ctx context.Context, tx *gorm.DB, assetID uuid.UUID) ([]models.Advisory, error) {
advisories := []models.Advisory{}
db := advisoryRepository.db.WithContext(ctx)
if tx != nil {
db = tx
}
err := db.Preload("AffectedPackages").Where("asset_id = ?", assetID).Find(&advisories).Error
return advisories, err
}

func (advisoryRepository *AdvisoryRepository) ReadAdvisory(ctx context.Context, tx *gorm.DB, id uuid.UUID) (models.Advisory, error) {
advisory := models.Advisory{}
db := advisoryRepository.db.WithContext(ctx)
if tx != nil {
db = tx
}
err := db.Preload("AffectedPackages").Where("id = ?", id).Find(&advisory).Error
return advisory, err
}

func (advisoryRepository *AdvisoryRepository) Update(ctx context.Context, tx *gorm.DB, id uuid.UUID, advisory *models.Advisory) error {
return advisoryRepository.GetDB(ctx, tx).Session(&gorm.Session{FullSaveAssociations: true}).Save(advisory).Error
}

func (advisoryRepository *AdvisoryRepository) Delete(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
db := advisoryRepository.db.WithContext(ctx)
if tx != nil {
db = tx
}
err := db.Preload("AffectedPackages").Delete(&models.Advisory{Model: models.Model{ID: id}}).Error
if err != nil {
return err
}
return nil
}
1 change: 1 addition & 0 deletions database/repositories/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ var Module = fx.Options(
fx.Provide(fx.Annotate(NewTrustedEntityRepository, fx.As(new(shared.TrustedEntityRepository)))),
fx.Provide(fx.Annotate(NewDependencyProxyRepository, fx.As(new(shared.DependencyProxySecretRepository)))),
fx.Provide(fx.Annotate(NewAdminRepository, fx.As(new(shared.AdminRepository)))),
fx.Provide(fx.Annotate(NewAdvisoryRepository, fx.As(new(shared.AdvisoryRepository)))),
)
53 changes: 53 additions & 0 deletions dtos/advisory_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (C) 2023 Tim Bastin, l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package dtos

import "github.com/google/uuid"

type AdvisoryCreate struct {
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
AffectedPackages []AffectedPackage `json:"affectedPackages"`
Severity string `json:"severity"`
VectorString string `json:"vectorString"`
AssetID uuid.UUID `json:"assetID"`
}
type AdvisoryUpdate struct {
Title *string `json:"title"`
Description *string `json:"description"`
AffectedPackages []AffectedPackage `json:"affectedPackages"`
Severity *string `json:"severity"`
VectorString *string `json:"vectorString"`
AssetID *uuid.UUID `json:"assetID"`
}

type AdvisoryDTO struct {
ID uuid.UUID `json:"id"`
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
AffectedPackages []AffectedPackage `json:"affectedPackages"`
Severity string `json:"severity"`
VectorString string `json:"vectorString"`
AssetID uuid.UUID `json:"assetID"`
}

type AffectedPackage struct {
ID uuid.UUID `json:"id,omitempty"`
Ecosystem string `json:"ecosystem"`
PackageName string `json:"packageName"`
SemverIntroduced *string `json:"semverStart"`
SemverFixed *string `json:"semverEnd"`
}
41 changes: 41 additions & 0 deletions router/advisory_router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (C) 2024 Tim Bastin, l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package router

import (
"github.com/l3montree-dev/devguard/controllers"
"github.com/l3montree-dev/devguard/shared"
"github.com/labstack/echo/v4"
)

type AdvisoryRouter struct {
*echo.Group
}

func NewAdvisoryRouter(
assetRepository shared.AssetRepository,
assetVersionGroup AssetVersionRouter,
advisoryController *controllers.AdvisoryController,
) AdvisoryRouter {
advisoryRouter := assetVersionGroup.Group.Group("/advisory")
advisoryRouter.POST("/", advisoryController.Create)
advisoryRouter.GET("/", advisoryController.ReadAll)
advisoryRouter.GET("/:id/", advisoryController.ReadAdvisory)
advisoryRouter.PATCH("/:id/", advisoryController.Update)
advisoryRouter.DELETE("/:id/", advisoryController.Delete)

return AdvisoryRouter{Group: advisoryRouter}
}
1 change: 1 addition & 0 deletions router/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ var RouterModule = fx.Options(
fx.Provide(NewVEXRuleRouter),
fx.Provide(NewExternalReferenceRouter),
fx.Provide(NewCrowdsourcedVexingRouter),
fx.Provide(NewAdvisoryRouter),
)
Loading
Loading