Skip to content

Commit 041a858

Browse files
authored
Merge pull request #1628 from l3montree-dev/1627-dependency-vulnerabilities-are-not-removed-after-deleting-the-related-artifact
fix artifact deletion logic and add tests for dependency vulnerability handling after delete artifacts
2 parents 4ba9dd0 + 455ec5f commit 041a858

5 files changed

Lines changed: 177 additions & 21 deletions

File tree

database/repositories/artifact_repository.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,21 @@ func (r *artifactRepository) ReadArtifact(name string, assetVersionName string,
5454
return artifact, err
5555
}
5656

57-
func (r *artifactRepository) DeleteArtifact(assetID uuid.UUID, assetVersionName string, artifactName string) error {
58-
err := r.db.Where("artifact_name = ? AND asset_version_name = ? AND asset_id = ?", artifactName, assetVersionName, assetID).Delete(&models.Artifact{}).Error
57+
func (r *artifactRepository) DeleteArtifact(tx *gorm.DB, assetID uuid.UUID, assetVersionName string, artifactName string) error {
58+
59+
db := r.db
60+
if tx != nil {
61+
db = tx
62+
}
63+
64+
err := db.Where("artifact_name = ? AND asset_version_name = ? AND asset_id = ?", artifactName, assetVersionName, assetID).Delete(&models.Artifact{}).Error
5965
if err != nil {
6066
return err
6167
}
6268

6369
r.FireAndForget(func() {
6470
sql := CleanupOrphanedRecordsSQL
65-
err = r.db.Exec(sql).Error
71+
err = db.Exec(sql).Error
6672
if err != nil {
6773
slog.Error("Failed to clean up orphaned records after deleting artifact", "err", err)
6874
}

mocks/mock_ArtifactRepository.go

Lines changed: 18 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/artifact_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func (s *ArtifactService) DeleteArtifact(assetID uuid.UUID, assetVersionName str
7474
}
7575

7676
// Delete the artifact record itself
77-
err = s.artifactRepository.GetDB(tx).Where("asset_id = ? AND asset_version_name = ? AND artifact_name = ?", assetID, assetVersionName, artifactName).Delete(&models.Artifact{}).Error
77+
err = s.artifactRepository.DeleteArtifact(tx, assetID, assetVersionName, artifactName)
7878
if err != nil {
7979
slog.Error("failed to delete artifact record", "assetID", assetID, "assetVersionName", assetVersionName, "artifactName", artifactName, "error", err)
8080
return err

shared/common_interfaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ type ArtifactRepository interface {
136136
utils.Repository[string, models.Artifact, DB]
137137
GetByAssetIDAndAssetVersionName(assetID uuid.UUID, assetVersionName string) ([]models.Artifact, error)
138138
ReadArtifact(name string, assetVersionName string, assetID uuid.UUID) (models.Artifact, error)
139-
DeleteArtifact(assetID uuid.UUID, assetVersionName string, artifactName string) error
139+
DeleteArtifact(tx DB, assetID uuid.UUID, assetVersionName string, artifactName string) error
140140
GetAllArtifactAffectedByDependencyVuln(tx DB, vulnID string) ([]models.Artifact, error)
141141
GetByAssetVersions(assetID uuid.UUID, assetVersionNames []string) ([]models.Artifact, error)
142142
}

tests/artifact_controller_test.go

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package tests
33
import (
44
"net/http/httptest"
55
"testing"
6-
"time"
76

87
"github.com/l3montree-dev/devguard/database/models"
98
"github.com/l3montree-dev/devguard/dtos"
@@ -124,9 +123,6 @@ func TestArtifactControllerDeleteArtifact(t *testing.T) {
124123
err := f.App.ArtifactController.DeleteArtifact(ctx)
125124
assert.NoError(t, err)
126125

127-
// Wait for Fire and Forget to complete
128-
time.Sleep(500 * time.Millisecond)
129-
130126
// Verify artifact was deleted
131127
var deletedArtifact models.Artifact
132128
err = f.DB.First(&deletedArtifact, "artifact_name = ? AND asset_id = ?", artifact1.ArtifactName, asset.ID).Error
@@ -216,5 +212,153 @@ func TestArtifactControllerDeleteArtifact(t *testing.T) {
216212
err = f.DB.First(&deletedArtifact, "artifact_name = ? AND asset_id = ?", artifact3.ArtifactName, asset.ID).Error
217213
assert.Error(t, err, "Artifact should be deleted")
218214
})
215+
216+
t.Run("should delete dependency vuln when it only belongs to the deleted artifact", func(t *testing.T) {
217+
// Create a new artifact for this test
218+
artifactSingle := models.Artifact{
219+
ArtifactName: "artifact-single-vuln",
220+
AssetVersionName: assetVersion.Name,
221+
AssetID: asset.ID,
222+
}
223+
assert.NoError(t, f.DB.Create(&artifactSingle).Error)
224+
225+
// Create a component for this test
226+
componentSingle := models.Component{
227+
ID: "pkg:npm/single-artifact-vuln-package@1.0.0",
228+
}
229+
assert.NoError(t, f.DB.Create(&componentSingle).Error)
230+
231+
// Create a vulnerability that only belongs to this artifact
232+
vulnSingle := models.DependencyVuln{
233+
Vulnerability: models.Vulnerability{
234+
AssetID: asset.ID,
235+
AssetVersionName: assetVersion.Name,
236+
State: dtos.VulnStateOpen,
237+
},
238+
CVEID: cve.CVE,
239+
ComponentPurl: componentSingle.ID,
240+
VulnerabilityPath: []string{"root", "artifact:artifact-single-vuln", componentSingle.ID},
241+
Artifacts: []models.Artifact{
242+
artifactSingle,
243+
},
244+
}
245+
assert.NoError(t, f.DB.Create(&vulnSingle).Error)
246+
247+
// Verify vulnerability exists before deletion
248+
var vulnBefore models.DependencyVuln
249+
assert.NoError(t, f.DB.First(&vulnBefore, "id = ?", vulnSingle.ID).Error)
250+
251+
// Create echo context for delete request
252+
e := echo.New()
253+
req := httptest.NewRequest("DELETE", "/", nil)
254+
rec := httptest.NewRecorder()
255+
ctx := e.NewContext(req, rec)
256+
257+
// Set context values
258+
ctx.Set("organization", org)
259+
ctx.Set("project", project)
260+
ctx.Set("asset", asset)
261+
ctx.Set("assetVersion", assetVersion)
262+
ctx.Set("artifact", artifactSingle)
263+
ctx.Set("session", NewSessionMock("test-user"))
264+
265+
// Execute delete
266+
err := f.App.ArtifactController.DeleteArtifact(ctx)
267+
assert.NoError(t, err)
268+
assert.Equal(t, 200, rec.Code)
269+
270+
// Verify artifact was deleted
271+
var deletedArtifact models.Artifact
272+
err = f.DB.First(&deletedArtifact, "artifact_name = ? AND asset_id = ?", artifactSingle.ArtifactName, asset.ID).Error
273+
assert.Error(t, err, "Artifact should be deleted")
274+
275+
// Verify the dependency vulnerability was also deleted
276+
var vulnAfter models.DependencyVuln
277+
err = f.DB.First(&vulnAfter, "id = ?", vulnSingle.ID).Error
278+
assert.Error(t, err, "Dependency vulnerability should be deleted when its only artifact is deleted")
279+
})
280+
281+
t.Run("should keep dependency vuln but remove artifact relation when vuln belongs to multiple artifacts", func(t *testing.T) {
282+
// Create two artifacts for this test
283+
artifactMulti1 := models.Artifact{
284+
ArtifactName: "artifact-multi-1",
285+
AssetVersionName: assetVersion.Name,
286+
AssetID: asset.ID,
287+
}
288+
assert.NoError(t, f.DB.Create(&artifactMulti1).Error)
289+
290+
artifactMulti2 := models.Artifact{
291+
ArtifactName: "artifact-multi-2",
292+
AssetVersionName: assetVersion.Name,
293+
AssetID: asset.ID,
294+
}
295+
assert.NoError(t, f.DB.Create(&artifactMulti2).Error)
296+
297+
// Create a component for this test
298+
componentMulti := models.Component{
299+
ID: "pkg:npm/multi-artifact-vuln-package@2.0.0",
300+
}
301+
assert.NoError(t, f.DB.Create(&componentMulti).Error)
302+
303+
// Create a vulnerability that belongs to both artifacts
304+
vulnMulti := models.DependencyVuln{
305+
Vulnerability: models.Vulnerability{
306+
AssetID: asset.ID,
307+
AssetVersionName: assetVersion.Name,
308+
State: dtos.VulnStateOpen,
309+
},
310+
CVEID: cve.CVE,
311+
ComponentPurl: componentMulti.ID,
312+
VulnerabilityPath: []string{"root", "artifact:artifact-multi-1", componentMulti.ID},
313+
Artifacts: []models.Artifact{
314+
artifactMulti1,
315+
artifactMulti2,
316+
},
317+
}
318+
assert.NoError(t, f.DB.Create(&vulnMulti).Error)
319+
320+
// Verify vulnerability exists with two artifacts before deletion
321+
var vulnBefore models.DependencyVuln
322+
assert.NoError(t, f.DB.Preload("Artifacts").First(&vulnBefore, "id = ?", vulnMulti.ID).Error)
323+
assert.Len(t, vulnBefore.Artifacts, 2, "Vulnerability should have 2 artifacts before deletion")
324+
325+
// Create echo context for delete request - delete artifact 1
326+
e := echo.New()
327+
req := httptest.NewRequest("DELETE", "/", nil)
328+
rec := httptest.NewRecorder()
329+
ctx := e.NewContext(req, rec)
330+
331+
// Set context values
332+
ctx.Set("organization", org)
333+
ctx.Set("project", project)
334+
ctx.Set("asset", asset)
335+
ctx.Set("assetVersion", assetVersion)
336+
ctx.Set("artifact", artifactMulti1)
337+
ctx.Set("session", NewSessionMock("test-user"))
338+
339+
// Execute delete
340+
err := f.App.ArtifactController.DeleteArtifact(ctx)
341+
assert.NoError(t, err)
342+
assert.Equal(t, 200, rec.Code)
343+
344+
// Verify artifact1 was deleted
345+
var deletedArtifact models.Artifact
346+
err = f.DB.First(&deletedArtifact, "artifact_name = ? AND asset_id = ?", artifactMulti1.ArtifactName, asset.ID).Error
347+
assert.Error(t, err, "Artifact1 should be deleted")
348+
349+
// Verify artifact2 still exists
350+
var remainingArtifact models.Artifact
351+
err = f.DB.First(&remainingArtifact, "artifact_name = ? AND asset_id = ?", artifactMulti2.ArtifactName, asset.ID).Error
352+
assert.NoError(t, err, "Artifact2 should still exist")
353+
354+
// Verify the dependency vulnerability still exists
355+
var vulnAfter models.DependencyVuln
356+
err = f.DB.Preload("Artifacts").First(&vulnAfter, "id = ?", vulnMulti.ID).Error
357+
assert.NoError(t, err, "Dependency vulnerability should still exist when it has remaining artifacts")
358+
359+
// Verify the vulnerability now only has one artifact (the remaining one)
360+
assert.Len(t, vulnAfter.Artifacts, 1, "Vulnerability should have only 1 artifact after deletion")
361+
assert.Equal(t, artifactMulti2.ArtifactName, vulnAfter.Artifacts[0].ArtifactName, "Remaining artifact should be artifact-multi-2")
362+
})
219363
})
220364
}

0 commit comments

Comments
 (0)