Skip to content

Commit dd680ef

Browse files
committed
Implement soft delete for Note and Package models
Update sync_vulnerablecode pipeline to handle deletions and updates. Signed-off-by: ziad hany <[email protected]>
1 parent 46953eb commit dd680ef

File tree

4 files changed

+161
-100
lines changed

4 files changed

+161
-100
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.1.2 on 2025-08-19 13:59
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("fedcode", "0005_remove_person_avatar"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="note",
15+
name="is_deleted",
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AddField(
19+
model_name="package",
20+
name="is_deleted",
21+
field=models.BooleanField(default=False),
22+
),
23+
]

fedcode/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
from federatedcode.settings import FEDERATEDCODE_WORKSPACE_LOCATION
2828

2929

30+
class ActiveManager(models.Manager):
31+
def get_queryset(self):
32+
return super().get_queryset().filter(is_deleted=False)
33+
34+
3035
class RemoteActor(models.Model):
3136
"""
3237
Represent a remote actor with its username
@@ -213,6 +218,16 @@ class Note(models.Model):
213218
Reputation,
214219
)
215220

221+
objects = ActiveManager() # Default manager (excludes deleted)
222+
all_objects = models.Manager() # Includes deleted
223+
224+
is_deleted = models.BooleanField(default=False)
225+
226+
def delete(self, *args, **kwargs):
227+
"""Soft delete instead of hard delete."""
228+
self.is_deleted = True
229+
self.save()
230+
216231
class Meta:
217232
ordering = ["-updated_at"]
218233

@@ -285,6 +300,16 @@ class Package(Actor):
285300
help_text="""the notes created by this package""",
286301
)
287302

303+
objects = ActiveManager() # Default manager (excludes deleted)
304+
all_objects = models.Manager() # Includes deleted
305+
306+
is_deleted = models.BooleanField(default=False)
307+
308+
def delete(self, *args, **kwargs):
309+
"""Soft delete instead of hard delete."""
310+
self.is_deleted = True
311+
self.save()
312+
288313
class Meta:
289314
ordering = ["purl"]
290315

fedcode/pipelines/sync_vulnerablecode.py

Lines changed: 110 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
from aboutcode.hashid import get_core_purl
1818
from aboutcode.pipeline import LoopProgress
19-
from fedcode.activitypub import Activity
20-
from fedcode.activitypub import UpdateActivity
21-
from fedcode.models import Note
2219
from fedcode.models import Package
2320
from fedcode.models import Repository
2421
from fedcode.models import Vulnerability
@@ -97,12 +94,16 @@ def sync_vulnerabilities(repository, logger):
9794
diff.change_type, repository.admin, yaml_data_a_blob, yaml_data_b_blob, logger
9895
)
9996

100-
if a_name == "purls.yml" or b_name == "purls.yml":
97+
elif a_name == "purls.yml" or b_name == "purls.yml":
10198
pkg_handler(
102-
diff.change_type, repository.admin, yaml_data_a_blob, yaml_data_b_blob, logger
99+
diff.change_type,
100+
repository.admin,
101+
yaml_data_a_blob,
102+
yaml_data_b_blob,
103+
logger,
103104
)
104105

105-
if a_name.startswith("VCID") or b_name.startswith("VCID"):
106+
elif a_name.startswith("VCID") or b_name.startswith("VCID"):
106107
vul_handler(diff.change_type, repository, yaml_data_a_blob, yaml_data_b_blob, logger)
107108

108109
repository.last_imported_commit = latest_commit_hash
@@ -112,116 +113,130 @@ def sync_vulnerabilities(repository, logger):
112113

113114
def vul_handler(change_type, repo_obj, yaml_data_a_blob, yaml_data_b_blob, logger):
114115
"""
115-
VCID-XXXX-XXXX-XXXX.yml
116+
Handle changes in VCID-XXXX-XXXX-XXXX.yml
116117
"""
117118
vulnerability_a_id = yaml_data_a_blob.get("vulnerability_id") if yaml_data_a_blob else None
118119
vulnerability_b_id = yaml_data_b_blob.get("vulnerability_id") if yaml_data_b_blob else None
119120

120-
if change_type == "A": # A for added paths
121+
if change_type == "A": # Added
121122
Vulnerability.objects.get_or_create(
122123
id=vulnerability_b_id,
123124
repo=repo_obj,
124125
)
125-
elif change_type in [
126-
"M",
127-
"R",
128-
]: # R for renamed paths , M for paths with modified data
126+
127+
elif change_type in ["M", "R"]: # Modified or Renamed
129128
with transaction.atomic():
130-
Vulnerability.objects.get(id=vulnerability_a_id, repo=repo_obj).delete()
131-
Vulnerability.objects.create(id=vulnerability_b_id, repo=repo_obj)
129+
if vulnerability_a_id and vulnerability_a_id != vulnerability_b_id:
130+
# renamed or id changed
131+
Vulnerability.objects.filter(id=vulnerability_a_id, repo=repo_obj).delete()
132+
133+
# update_or_create to avoid losing relations unnecessarily
134+
Vulnerability.objects.update_or_create(
135+
id=vulnerability_b_id,
136+
repo=repo_obj,
137+
defaults={}, # add fields parsed from yaml_data_b_blob here
138+
)
139+
140+
elif change_type == "D": # Deleted
141+
if vulnerability_a_id:
142+
Vulnerability.objects.filter(id=vulnerability_a_id, repo=repo_obj).delete()
132143

133-
elif change_type == "D": # D for deleted paths
134-
Vulnerability.objects.get(
135-
id=yaml_data_b_blob.get("vulnerability_id"),
136-
repo=repo_obj,
137-
).delete()
138144
else:
139145
logger(f"Invalid Vulnerability File", level=logging.ERROR)
140146

141147

142148
def pkg_handler(change_type, default_service, yaml_data_a_blob, yaml_data_b_blob, logger):
143149
"""
144-
purls.yml
150+
Handle changes in purls.yml
145151
"""
146152

147-
if change_type == "A":
148-
for purl in yaml_data_b_blob:
153+
if change_type == "A": # Added packages
154+
for purl in yaml_data_b_blob or []:
149155
core_purl = get_core_purl(purl)
150-
pkg, _ = Package.objects.get_or_create(purl=core_purl, service=default_service)
151-
152-
# elif change_type == "M":
153-
# pkg = Package.objects.get(purl=package_a, service=default_service)
154-
# pkg.purl = package_b
155-
# pkg.save()
156-
#
157-
# for version_a, version_b in zip_longest(
158-
# yaml_data_a_blob, yaml_data_b_blob
159-
# ):
160-
# if version_b and not version_a:
161-
# utils.create_note(pkg, version_b)
162-
#
163-
# if version_a and not version_b:
164-
# utils.delete_note(pkg, version_a)
165-
#
166-
# if version_a and version_b:
167-
# note = Note.objects.get(acct=pkg.acct, content=saneyaml.dump(version_a))
168-
# if note.content == saneyaml.dump(version_b):
169-
# continue
170-
#
171-
# note.content = saneyaml.dump(version_b)
172-
# note.save()
173-
#
174-
# update_activity = UpdateActivity(actor=pkg.to_ap, object=note.to_ap)
175-
# Activity.federate(
176-
# targets=pkg.followers_inboxes,
177-
# body=update_activity.to_ap(),
178-
# key_id=pkg.key_id,
179-
# )
180-
#
181-
# elif change_type == "D":
182-
# pkg = Package.objects.get(purl=package_a, service=default_service)
183-
# for version in yaml_data_a_blob:
184-
# utils.delete_note(pkg, version)
185-
# pkg.delete()
156+
Package.objects.get_or_create(purl=core_purl, service=default_service)
186157

158+
elif change_type == "M": # Modified packages
159+
for package_a, package_b in zip_longest(yaml_data_a_blob or [], yaml_data_b_blob or []):
160+
if not package_a or not package_b:
161+
continue # skip if one side missing
187162

188-
def note_handler(change_type, default_service, yaml_data_a_blob, yaml_data_b_blob, logger):
189-
"""
190-
vulnerabilities.yml
191-
"""
192-
if change_type == "A":
193-
for pkg_status in yaml_data_b_blob:
194-
purl = pkg_status.get("purl")
163+
core_purl_a = get_core_purl(package_a)
164+
core_purl_b = get_core_purl(package_b)
165+
166+
try:
167+
pkg = Package.objects.get(purl=core_purl_a, service=default_service)
168+
except Package.DoesNotExist:
169+
logger(f"Package not found for {core_purl_a}", level=logging.ERROR)
170+
continue
171+
172+
# Update package purl if changed
173+
if pkg.purl != core_purl_b:
174+
pkg.purl = core_purl_b
175+
pkg.save()
176+
177+
elif change_type == "D": # Deleted packages
178+
for purl in yaml_data_a_blob or []:
195179
if not purl:
196-
logger(f"Invalid Vulnerability File", level=logging.ERROR)
197-
return
180+
logger("Invalid PURL in deleted entry", level=logging.ERROR)
181+
continue
198182
core_purl = get_core_purl(purl)
199-
pkg_b, _ = Package.objects.get_or_create(purl=core_purl, service=default_service)
200-
temp = saneyaml.dump(pkg_status)
201-
utils.create_note(pkg_b, temp)
202-
203-
# elif change_type == "M":
204-
# for pkg_status_a, pkg_status_b in zip_longest(
205-
# yaml_data_a_blob, yaml_data_b_blob
206-
# ):
207-
# if pkg_status_a and not pkg_status_b:
208-
# utils.create_note(pkg_a, pkg_status_b)
209-
#
210-
# if pkg_status_a and not pkg_status_b:
211-
# utils.delete_note(pkg_a, pkg_status_b)
212-
#
213-
# if pkg_status_a and pkg_status_b:
214-
# utils.update_note(pkg_a, saneyaml.dump(pkg_status_a), saneyaml.dump(pkg_status_b))
215-
#
216-
# elif change_type == "D":
217-
# for pkg_status in yaml_data_a_blob:
218-
# purl = pkg_status.get("purl")
219-
# if not purl:
220-
# logger(f"Invalid Vulnerability File", level=logging.ERROR)
221-
# return
222-
# core_purl = get_core_purl(purl)
223-
# pkg_a, _ = Package.objects.get_or_create(purl=core_purl, service=default_service)
224-
# temp = saneyaml.dump(pkg_status)
225-
# utils.delete_note(pkg_a, temp)
183+
try:
184+
pkg = Package.objects.get(purl=core_purl, service=default_service)
185+
pkg.delete()
186+
except Package.DoesNotExist:
187+
logger(f"Package not found for deletion: {core_purl}", level=logging.WARNING)
188+
226189
else:
227-
logger(f"Invalid Vulnerability File", level=logging.ERROR)
190+
logger(f"Unknown change_type: {change_type}", level=logging.ERROR)
191+
192+
193+
def note_handler(change_type, default_service, yaml_data_a_blob, yaml_data_b_blob, logger):
194+
"""
195+
Handle notes from vulnerabilities.yml changes.
196+
Uses zip_longest so both old (A) and new (B) entries are processed together.
197+
"""
198+
199+
for pkg_status_a, pkg_status_b in zip_longest(yaml_data_a_blob or [], yaml_data_b_blob or []):
200+
pkg_a = pkg_b = None
201+
202+
# Resolve old package
203+
if pkg_status_a:
204+
purl_a = pkg_status_a.get("purl")
205+
if not purl_a:
206+
logger("Invalid Vulnerability File: missing purl in old entry", level=logging.ERROR)
207+
else:
208+
core_purl_a = get_core_purl(purl_a)
209+
pkg_a, _ = Package.objects.get_or_create(purl=core_purl_a, service=default_service)
210+
211+
# Resolve new package
212+
if pkg_status_b:
213+
purl_b = pkg_status_b.get("purl")
214+
if not purl_b:
215+
logger("Invalid Vulnerability File: missing purl in new entry", level=logging.ERROR)
216+
else:
217+
core_purl_b = get_core_purl(purl_b)
218+
pkg_b, _ = Package.objects.get_or_create(purl=core_purl_b, service=default_service)
219+
220+
if change_type == "A": # Added entries
221+
if pkg_status_b and pkg_b:
222+
utils.create_note(pkg_b, saneyaml.dump(pkg_status_b))
223+
224+
elif change_type == "M": # Modified entries
225+
# Deleted entry
226+
if pkg_status_a and not pkg_status_b and pkg_a:
227+
utils.delete_note(pkg_a, saneyaml.dump(pkg_status_a))
228+
229+
# Added entry
230+
elif pkg_status_b and not pkg_status_a and pkg_b:
231+
utils.create_note(pkg_b, saneyaml.dump(pkg_status_b))
232+
233+
# Updated entry
234+
elif pkg_status_a and pkg_status_b and pkg_b:
235+
utils.update_note(pkg_b, saneyaml.dump(pkg_status_a), saneyaml.dump(pkg_status_b))
236+
237+
elif change_type == "D": # Deleted entries
238+
if pkg_status_a and pkg_a:
239+
utils.delete_note(pkg_a, saneyaml.dump(pkg_status_a))
240+
241+
else:
242+
logger(f"Unknown change_type: {change_type}", level=logging.ERROR)

fedcode/pipes/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020

2121

2222
def create_note(pkg, note_dict):
23-
# TODO: also take argument for source of the note ideally github blob for
24-
# for file.
23+
# TODO: also take argument for source of the note ideally github blob for file.
2524
note, _ = Note.objects.get_or_create(acct=pkg.acct, content=saneyaml.dump(note_dict))
2625
pkg.notes.add(note)
2726
create_activity = CreateActivity(actor=pkg.to_ap, object=note.to_ap)
@@ -52,8 +51,7 @@ def update_note(pkg, old_note_dict, new_note_dict):
5251
def delete_note(pkg, note_dict):
5352
note = Note.objects.get(acct=pkg.acct, content=saneyaml.dump(note_dict))
5453
note_ap = note.to_ap
55-
note.delete()
56-
pkg.notes.remove(note)
54+
note.delete() # soft delete
5755

5856
deleted_activity = DeleteActivity(actor=pkg.to_ap, object=note_ap)
5957
Activity.federate(
@@ -87,7 +85,7 @@ def get_scan_note(path):
8785
purl = package_metadata_path_to_purl(path=path)
8886

8987
# TODO: Use tool-alias.yml to get tool for corresponding tool
90-
# for scan https://github.com/aboutcode-org/federatedcode/issues/24
88+
# for scan https://github.com/aboutcode-org/federatedcode/issues/24
9189
return {
9290
"purl": str(purl),
9391
"scans": [

0 commit comments

Comments
 (0)