Skip to content

Commit feb4a99

Browse files
committed
fix: ensure that "empty" object fields on advisories are handled correctly
1 parent add70f4 commit feb4a99

6 files changed

Lines changed: 74 additions & 16 deletions

File tree

ruff.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ select = [
1919
known-first-party = [
2020
'generate_osv_advisories',
2121
'user_agent',
22-
'typings'
22+
'typings',
23+
'utils'
2324
]

scripts/download_sa_advisories.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import json
1111
import os
12-
import typing
1312

1413
import requests
1514

15+
import utils
1616
from typings import drupal
1717
from user_agent import user_agent
1818

@@ -26,11 +26,10 @@ def get_most_recent_changed_timestamp() -> int:
2626
for file in os.scandir('cache/advisories'):
2727
if not file.is_file() or not file.name.endswith('.json'):
2828
continue
29-
with open(file.path) as f:
30-
advisory = typing.cast(drupal.Advisory, json.load(f))
31-
changed = int(advisory['changed'])
32-
if changed > most_recent_changed or most_recent_changed == 0:
33-
most_recent_changed = changed
29+
advisory = utils.load_sa_advisory(file.path)
30+
changed = int(advisory['changed'])
31+
if changed > most_recent_changed or most_recent_changed == 0:
32+
most_recent_changed = changed
3433
except FileNotFoundError:
3534
pass
3635
return most_recent_changed

scripts/generate_osv_advisories.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import semver
1818
from markdownify import markdownify
1919

20+
import utils
2021
from typings import drupal, osv
2122
from user_agent import user_agent
2223

@@ -318,6 +319,9 @@ def get_credits_from_sa(
318319

319320

320321
def determine_composer_package_name(sa_advisory: drupal.Advisory) -> str:
322+
if sa_advisory['field_project'] is None:
323+
raise Exception('advisory does not have a project!')
324+
321325
project = typing.cast(
322326
drupal.Project, fetch_drupal_node(sa_advisory['field_project']['id'])
323327
)
@@ -425,8 +429,7 @@ def generate_osv_advisories() -> None:
425429
if not file.is_file() or not file.name.endswith('.json'):
426430
continue
427431

428-
with open(file.path) as f:
429-
sa_advisory: drupal.Advisory = json.load(f)
432+
sa_advisory = utils.load_sa_advisory(file.path)
430433
print(f'processing {sa_advisory["url"]}')
431434
sa_id = file.name.removesuffix('.json')
432435
osv_advisory = build_osv_advisory(sa_id, sa_advisory)

scripts/precache_nodes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import requests
1414

15+
import utils
1516
from typings import drupal
1617
from user_agent import user_agent
1718

@@ -49,9 +50,10 @@ def fetch_and_cache_drupal_nodes() -> None:
4950
if not file.is_file() or not file.name.endswith('.json'):
5051
continue
5152

52-
with open(file.path) as f:
53-
sa_advisory: drupal.Advisory = json.load(f)
54-
ids.add(sa_advisory['field_project']['id'])
53+
sa_advisory = utils.load_sa_advisory(file.path)
54+
55+
if sa_advisory['field_project'] is not None:
56+
ids.add(sa_advisory['field_project']['id'])
5557

5658
for i, batch in enumerate(batched(ids, 50, strict=False)):
5759
print(f'fetching {len(batch)} nodes ({len(ids) - i * 50 - len(batch)} remaining)')

scripts/typings/drupal.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,42 @@ class Node(typing.TypedDict):
1717
type: str
1818

1919

20-
class Advisory(Node):
20+
class AdvisoryBase(Node):
2121
field_is_psa: typing.Literal['0', '1']
2222
field_affected_versions: str | None
23-
field_project: EntityReferenceField
2423
field_fixed_in: list[EntityReferenceField]
25-
field_sa_reported_by: RichTextField | list[typing.Never]
2624
field_sa_criticality: str
2725
field_sa_cve: list[str]
28-
field_sa_description: RichTextField
2926
created: str
3027
changed: str
3128
title: str
3229
url: str
3330

3431

32+
class Advisory(AdvisoryBase):
33+
"""
34+
Represents an advisory sourced from the Drupal JSON API that has been
35+
transformed to make it easier to work with
36+
"""
37+
38+
field_project: EntityReferenceField | None
39+
field_sa_reported_by: RichTextField
40+
field_sa_description: RichTextField
41+
42+
43+
class AdvisoryRaw(AdvisoryBase):
44+
"""
45+
Represents an advisory provided by the Drupal JSON API without any post-processing.
46+
47+
This mainly means that object fields which don't have a value in the database
48+
will be represented by an empty list due to how associated arrays in PHP work
49+
"""
50+
51+
field_project: EntityReferenceField | list[typing.Never]
52+
field_sa_reported_by: RichTextField | list[typing.Never]
53+
field_sa_description: RichTextField | list[typing.Never]
54+
55+
3556
class Project(Node):
3657
# type will be project_module, project_theme, or project_core
3758
field_project_machine_name: str

scripts/utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import json
2+
3+
from typings import drupal
4+
5+
6+
def load_sa_advisory(file_path: str) -> drupal.Advisory:
7+
"""
8+
Loads a Drupal advisory from a json file stored on disk, making some adjustments
9+
in the process to make it easier to work with
10+
"""
11+
with open(file_path) as f:
12+
raw_advisory: drupal.AdvisoryRaw = json.load(f)
13+
14+
# noinspection PyTypeChecker
15+
# https://youtrack.jetbrains.com/issue/PY-58714/False-positive-TypedDict-has-missing-key-when-using-unpacking
16+
sa_advisory: drupal.Advisory = {
17+
**raw_advisory,
18+
'field_project': None,
19+
'field_sa_reported_by': {'format': '1', 'value': ''},
20+
'field_sa_description': {'format': '1', 'value': ''},
21+
}
22+
23+
if isinstance(raw_advisory['field_project'], dict):
24+
sa_advisory['field_project'] = raw_advisory['field_project']
25+
26+
if isinstance(raw_advisory['field_sa_reported_by'], dict):
27+
sa_advisory['field_sa_reported_by'] = raw_advisory['field_sa_reported_by']
28+
29+
if isinstance(raw_advisory['field_sa_description'], dict):
30+
sa_advisory['field_sa_description'] = raw_advisory['field_sa_description']
31+
32+
return sa_advisory

0 commit comments

Comments
 (0)