Skip to content

Commit 742b2b1

Browse files
Nils KreinerNils Kreiner
authored andcommitted
WIP: load new cve's via cpe
1 parent 09d1739 commit 742b2b1

File tree

16 files changed

+289
-8
lines changed

16 files changed

+289
-8
lines changed

backend/__init__.py

Whitespace-only changes.

backend/analyzer/manager/project_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.db import DatabaseError
55

66
from analyzer.manager.cve_manager import CVEObjectManager
7-
from analyzer.models import Project, Report, Dependency
7+
from analyzer.models import Project, Report, Dependency, CPEObject
88
from analyzer.parser.types import ParseResult
99
from utilities.helperclass import hash_key
1010

@@ -124,6 +124,12 @@ def _update_dependencies(self, data: dict[str, ParseResult]):
124124
dependency_object.path = data.get(new_dependency_id).path
125125
dependency_object.in_use = True
126126
dependency_object.save()
127+
for new_cpe_id in data.get(new_dependency_id).cpe_ids:
128+
cpe_object = CPEObject.objects.get_or_create(
129+
dependency=dependency_object,
130+
cpe_id=new_cpe_id
131+
)[0]
132+
cpe_object.save()
127133

128134
for vulnerability in data.get(new_dependency_id).vulnerabilities:
129135
cve_object = CVEObjectManager(vulnerability).get()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.1.2 on 2025-03-04 15:10
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('analyzer', '0002_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='project',
15+
name='auto_update',
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AlterField(
19+
model_name='dependency',
20+
name='package_manager',
21+
field=models.CharField(default='NA', max_length=255),
22+
),
23+
migrations.AlterField(
24+
model_name='report',
25+
name='overall_cvss_severity',
26+
field=models.CharField(max_length=255, null=True),
27+
),
28+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.1.2 on 2025-03-11 18:07
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('analyzer', '0003_project_auto_update_alter_dependency_package_manager_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='CPEObject',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('cpe_id', models.CharField(max_length=255, unique=True)),
19+
('dependency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='analyzer.dependency')),
20+
],
21+
options={
22+
'db_table': 'securecheckplus_cpe_object',
23+
},
24+
),
25+
]

backend/analyzer/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Meta:
1919
choices=constants.Threshold.choices,
2020
default=constants.Threshold.MEDIUM.name)
2121
api_key_hash = models.CharField(null=True, max_length=100)
22+
auto_update = models.BooleanField(default=False)
2223

2324
@property
2425
def dependency_count(self):
@@ -147,3 +148,10 @@ class Meta:
147148
score_metric_data = models.TextField(default=constants.DEFAULT_SCORE_METRIC_DATA)
148149
overall_cvss_score = models.DecimalField(max_digits=3, decimal_places=1, null=True)
149150
overall_cvss_severity = models.CharField(max_length=255, null=True)
151+
152+
class CPEObject(models.Model):
153+
class Meta:
154+
db_table = constants.DB_SCHEMA_PREFIX + "cpe_object"
155+
156+
dependency = models.ForeignKey(Dependency, on_delete=models.CASCADE)
157+
cpe_id = models.CharField(max_length=255, blank=False, unique=True)

backend/analyzer/parser/owasp_parser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,12 @@ def parse_json(json_data: str or dict) -> dict[str, ParseResult]:
9999
dependency.get("vulnerabilities", []) if
100100
vuln["source"] in ["NVD", "OSSINDEX"]]
101101

102+
cpe_ids = [vuln["id"] for vuln in
103+
dependency.get("vulnerabilityIds", [])]
104+
102105
result = ParseResult(dependency_name, version, path,
103106
dependency_license,
104-
vulnerabilities, package_manager)
107+
vulnerabilities, package_manager, cpe_ids)
105108
data[f"{dependency_name}:{version}"] = result
106109

107110
return data

backend/analyzer/parser/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ class ParseResult:
99
license: str
1010
vulnerabilities: list[str]
1111
package_manager: str
12+
cpe_ids: list[str]

backend/analyzer/services/cve_fetcher.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ class CVEFetcher:
5555
"baseSeverity": "N/A"
5656
}
5757

58-
def __init__(self, cve_id: str):
58+
def __init__(self, cve_id: str = None, cpe_id: str = None):
5959
"""
6060
Initializes the CVEFetcher with a specific CVE ID.
6161
6262
Args:
6363
cve_id (str): The CVE identifier to fetch data for.
6464
"""
6565
self.cve_id = cve_id
66+
self.cpe_id = cpe_id
6667
self.data = {}
6768
self.successful = False
6869

@@ -78,7 +79,7 @@ def fetch_from_nist_gov(self):
7879
"""
7980
try:
8081
headers = {"apiKey": NVD_API_KEY}
81-
url = parse.urlunparse(NVD_ADDRESS) + self.cve_id
82+
url = parse.urlunparse(NVD_ADDRESS) + "?cveId=" + self.cve_id
8283
logger.info(f"Fetching CVE data from NIST for CVE ID: {self.cve_id} using URL: {url}")
8384
response = requests.get(url, headers=headers)
8485

@@ -127,6 +128,71 @@ def fetch_from_nist_gov(self):
127128
except (ValueError, KeyError) as e:
128129
logger.error(f"Failed to retrieve or process CVE data for CVE ID: {self.cve_id}. Error: {e}")
129130

131+
def fetch_from_nist_by_cpe(self):
132+
"""
133+
Fetches CVE data from the NIST government server for a given CPE ID.
134+
135+
The function will retrieve all CVEs associated with the provided CPE ID.
136+
"""
137+
if not self.cpe_id:
138+
logger.error("CPE ID is required for this fetcher.")
139+
return
140+
141+
try:
142+
headers = {"apiKey": NVD_API_KEY}
143+
url = parse.urlunparse(NVD_ADDRESS) + "?cpeName=" + self.cpe_id
144+
logger.info(f"Fetching CVE data from NIST for CPE ID: {self.cpe_id} using URL: {url}")
145+
response = self._make_request(url, headers)
146+
147+
if response:
148+
self._process_cve_data(response)
149+
self.successful = True
150+
logger.info(f"Successfully fetched CVE data for CPE ID: {self.cpe_id}")
151+
152+
except Exception as e:
153+
logger.error(f"Failed to fetch CVE data for CPE ID: {self.cpe_id}. Error: {e}")
154+
155+
156+
def _make_request(self, url, headers):
157+
"""
158+
Makes a GET request to the provided URL with the given headers and handles the response.
159+
"""
160+
try:
161+
response = requests.get(url, headers=headers)
162+
163+
if response.status_code != 200:
164+
logger.warning(f"Failed to fetch CVE data for CPE ID: {self.cpe_id}. HTTP status: {response.status_code}")
165+
return None
166+
167+
return response.json()
168+
169+
except requests.RequestException as e:
170+
logger.error(f"Request failed for CPE ID: {self.cpe_id}. Error: {e}")
171+
return None
172+
173+
def _process_cve_data(self, response_json):
174+
"""
175+
Processes the CVE data from the response and fetches additional CVE data for each CVE ID.
176+
"""
177+
if not isinstance(response_json, dict) or "vulnerabilities" not in response_json:
178+
raise ValueError(f"Invalid response structure for CPE ID: {self.cpe_id}")
179+
180+
cve_list = response_json["vulnerabilities"]
181+
for cve_entry in cve_list:
182+
cve_id = cve_entry.get("cve", {}).get("id", None)
183+
if cve_id:
184+
self.fetch_cve_by_id(cve_id)
185+
186+
def fetch_cve_by_id(self, cve_id):
187+
"""
188+
Fetches CVE data for a specific CVE ID (helper function for fetching individual CVEs).
189+
"""
190+
# Reuse the original `fetch_from_nist_gov` logic here for individual CVEs
191+
logger.info(f"Fetching individual CVE data for {cve_id}")
192+
fetcher = CVEFetcher(cve_id=cve_id)
193+
fetcher.fetch_from_nist_gov()
194+
self.data[cve_id] = fetcher.data
195+
130196
def fetch_epss(self):
131197
"""
132198
Fetches the Exploit Prediction Scoring System (EPSS) score for the given CVE ID.

backend/securecheckplus/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def get_env_variable_or_shutdown_gracefully(var_name):
157157
"django.contrib.messages",
158158
"django.contrib.staticfiles",
159159
"rest_framework",
160+
"django_apscheduler",
160161
]
161162

162163
MIDDLEWARE = [

backend/utilities/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
SERVER_MAIL_ADDRESS = "[email protected]"
55

6-
NVD_ADDRESS = parse.urlparse("https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=")
6+
NVD_ADDRESS = parse.urlparse("https://services.nvd.nist.gov/rest/json/cves/2.0")
77

88
EPSS_ADDRESS = parse.urlparse("https://api.first.org/data/v1/epss?cve=")
99

0 commit comments

Comments
 (0)