Skip to content

Commit 9847b2e

Browse files
authored
Merge branch 'main' into add-cwe-support-in-multiple-importers
2 parents 3a2025b + fabe035 commit 9847b2e

18 files changed

+886
-556
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ else
4242
SUDO_POSTGRES=
4343
endif
4444

45+
ifeq ($(UNAME), Darwin)
46+
GET_SECRET_KEY=`head /dev/urandom | base64 | head -c50`
47+
endif
48+
4549
virtualenv:
4650
@echo "-> Bootstrap the virtualenv with PYTHON_EXE=${PYTHON_EXE}"
4751
@${PYTHON_EXE} ${VIRTUALENV_PYZ} --never-download --no-periodic-update ${VENV}

vulnerabilities/importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def to_dict(self):
111111
def from_dict(cls, ref: dict):
112112
return cls(
113113
reference_id=ref["reference_id"],
114-
reference_type=ref["reference_type"],
114+
reference_type=ref.get("reference_type") or "",
115115
url=ref["url"],
116116
severities=[
117117
VulnerabilitySeverity.from_dict(severity) for severity in ref["severities"]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from datetime import datetime
2+
from datetime import timezone
3+
4+
from aboutcode.pipeline import LoopProgress
5+
from django.db import migrations
6+
from packageurl import PackageURL
7+
8+
CHUNK_SIZE = 50000
9+
BATCH_SIZE = 500
10+
11+
12+
class Migration(migrations.Migration):
13+
def fix_alpine_purl_type(apps, schema_editor):
14+
"""Use proper apk package type for Alpine"""
15+
16+
Package = apps.get_model("vulnerabilities", "Package")
17+
batch = []
18+
alpine_packages_query = Package.objects.filter(type="alpine")
19+
20+
log(f"\nFixing PURL for {alpine_packages_query.count():,d} alpine packages")
21+
progress = LoopProgress(
22+
total_iterations=alpine_packages_query.count(),
23+
progress_step=10,
24+
logger=log,
25+
)
26+
for package in progress.iter(alpine_packages_query.iterator(chunk_size=CHUNK_SIZE)):
27+
package.type = "apk"
28+
package.namespace = "alpine"
29+
30+
package.package_url = update_alpine_purl(package.package_url, "apk", "alpine")
31+
package.plain_package_url = update_alpine_purl(
32+
package.plain_package_url, "apk", "alpine"
33+
)
34+
35+
batch.append(package)
36+
if len(batch) >= BATCH_SIZE:
37+
bulk_update_package(Package, batch)
38+
batch.clear()
39+
40+
bulk_update_package(Package, batch)
41+
42+
def reverse_fix_alpine_purl_type(apps, schema_editor):
43+
Package = apps.get_model("vulnerabilities", "Package")
44+
batch = []
45+
alpine_packages_query = Package.objects.filter(type="apk", namespace="alpine")
46+
47+
log(f"\nREVERSE: Fix for {alpine_packages_query.count():,d} alpine packages")
48+
progress = LoopProgress(
49+
total_iterations=alpine_packages_query.count(),
50+
progress_step=10,
51+
logger=log,
52+
)
53+
for package in progress.iter(alpine_packages_query.iterator(chunk_size=CHUNK_SIZE)):
54+
package.type = "alpine"
55+
package.namespace = ""
56+
57+
package.package_url = update_alpine_purl(package.package_url, "alpine", "")
58+
package.plain_package_url = update_alpine_purl(package.plain_package_url, "alpine", "")
59+
60+
batch.append(package)
61+
if len(batch) >= BATCH_SIZE:
62+
bulk_update_package(Package, batch)
63+
batch.clear()
64+
65+
bulk_update_package(Package, batch)
66+
67+
dependencies = [
68+
("vulnerabilities", "0087_update_alpine_advisory_created_by"),
69+
]
70+
71+
operations = [
72+
migrations.RunPython(
73+
code=fix_alpine_purl_type,
74+
reverse_code=reverse_fix_alpine_purl_type,
75+
),
76+
]
77+
78+
79+
def bulk_update_package(package, batch):
80+
if batch:
81+
package.objects.bulk_update(
82+
objs=batch,
83+
fields=[
84+
"type",
85+
"namespace",
86+
"package_url",
87+
"plain_package_url",
88+
],
89+
)
90+
91+
92+
def update_alpine_purl(purl, purl_type, purl_namespace):
93+
package_url = PackageURL.from_string(purl).to_dict()
94+
package_url["type"] = purl_type
95+
package_url["namespace"] = purl_namespace
96+
return str(PackageURL(**package_url))
97+
98+
99+
def log(message):
100+
now_local = datetime.now(timezone.utc).astimezone()
101+
timestamp = now_local.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
102+
message = f"{timestamp} {message}"
103+
print(message)

vulnerabilities/models.py

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import csv
1011
import hashlib
1112
import json
1213
import logging
13-
import typing
14+
import xml.etree.ElementTree as ET
1415
from contextlib import suppress
1516
from functools import cached_property
16-
from typing import Optional
17+
from itertools import groupby
18+
from operator import attrgetter
1719
from typing import Union
1820

21+
from cvss.exceptions import CVSS2MalformedError
22+
from cvss.exceptions import CVSS3MalformedError
23+
from cvss.exceptions import CVSS4MalformedError
1924
from cwe2.database import Database
25+
from cwe2.mappings import xml_database_path
26+
from cwe2.weakness import Weakness as DBWeakness
2027
from django.contrib.auth import get_user_model
2128
from django.contrib.auth.models import UserManager
2229
from django.core import exceptions
@@ -43,8 +50,8 @@
4350
from univers.version_range import AlpineLinuxVersionRange
4451
from univers.versions import Version
4552

46-
from aboutcode import hashid
4753
from vulnerabilities import utils
54+
from vulnerabilities.severity_systems import EPSS
4855
from vulnerabilities.severity_systems import SCORING_SYSTEMS
4956
from vulnerabilities.utils import normalize_purl
5057
from vulnerabilities.utils import purl_to_dict
@@ -56,7 +63,7 @@
5663
models.CharField.register_lookup(Trim)
5764

5865
# patch univers for missing entry
59-
RANGE_CLASS_BY_SCHEMES["alpine"] = AlpineLinuxVersionRange
66+
RANGE_CLASS_BY_SCHEMES["apk"] = AlpineLinuxVersionRange
6067

6168

6269
class BaseQuerySet(models.QuerySet):
@@ -373,6 +380,127 @@ def get_related_purls(self):
373380
"""
374381
return [p.package_url for p in self.packages.distinct().all()]
375382

383+
def aggregate_fixed_and_affected_packages(self):
384+
from vulnerabilities.utils import get_purl_version_class
385+
386+
sorted_fixed_by_packages = self.fixed_by_packages.filter(is_ghost=False).order_by(
387+
"type", "namespace", "name", "qualifiers", "subpath"
388+
)
389+
390+
if sorted_fixed_by_packages:
391+
sorted_fixed_by_packages.first().calculate_version_rank
392+
393+
sorted_affected_packages = self.affected_packages.all()
394+
395+
if sorted_affected_packages:
396+
sorted_affected_packages.first().calculate_version_rank
397+
398+
grouped_fixed_by_packages = {
399+
key: list(group)
400+
for key, group in groupby(
401+
sorted_fixed_by_packages,
402+
key=attrgetter("type", "namespace", "name", "qualifiers", "subpath"),
403+
)
404+
}
405+
406+
all_affected_fixed_by_matches = []
407+
408+
for sorted_affected_package in sorted_affected_packages:
409+
affected_fixed_by_matches = {
410+
"affected_package": sorted_affected_package,
411+
"matched_fixed_by_packages": [],
412+
}
413+
414+
# Build the key to find matching group
415+
key = (
416+
sorted_affected_package.type,
417+
sorted_affected_package.namespace,
418+
sorted_affected_package.name,
419+
sorted_affected_package.qualifiers,
420+
sorted_affected_package.subpath,
421+
)
422+
423+
# Get matching group from pre-grouped fixed_by_packages
424+
matching_fixed_packages = grouped_fixed_by_packages.get(key, [])
425+
426+
# Get version classes for comparison
427+
affected_version_class = get_purl_version_class(sorted_affected_package)
428+
affected_version = affected_version_class(sorted_affected_package.version)
429+
430+
# Compare versions and filter valid matches
431+
matched_fixed_by_packages = [
432+
fixed_by_package.purl
433+
for fixed_by_package in matching_fixed_packages
434+
if get_purl_version_class(fixed_by_package)(fixed_by_package.version)
435+
> affected_version
436+
]
437+
438+
affected_fixed_by_matches["matched_fixed_by_packages"] = matched_fixed_by_packages
439+
all_affected_fixed_by_matches.append(affected_fixed_by_matches)
440+
return sorted_fixed_by_packages, sorted_affected_packages, all_affected_fixed_by_matches
441+
442+
def get_severity_vectors_and_values(self):
443+
"""
444+
Collect severity vectors and values, excluding EPSS scoring systems and handling errors gracefully.
445+
"""
446+
severity_vectors = []
447+
severity_values = set()
448+
449+
# Exclude EPSS scoring system
450+
base_severities = self.severities.exclude(scoring_system=EPSS.identifier)
451+
452+
# QuerySet for severities with valid scoring_elements and scoring_system in SCORING_SYSTEMS
453+
valid_scoring_severities = base_severities.filter(
454+
scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.keys()
455+
)
456+
457+
for severity in valid_scoring_severities:
458+
try:
459+
vector_values = SCORING_SYSTEMS[severity.scoring_system].get(
460+
severity.scoring_elements
461+
)
462+
if vector_values:
463+
severity_vectors.append(vector_values)
464+
except (
465+
CVSS2MalformedError,
466+
CVSS3MalformedError,
467+
CVSS4MalformedError,
468+
NotImplementedError,
469+
) as e:
470+
logging.error(f"CVSSMalformedError for {severity.scoring_elements}: {e}")
471+
472+
valid_value_severities = base_severities.filter(value__isnull=False).exclude(value="")
473+
474+
severity_values.update(valid_value_severities.values_list("value", flat=True))
475+
476+
return severity_vectors, severity_values
477+
478+
479+
def get_cwes(self):
480+
"""Yield CWE Weakness objects"""
481+
for cwe_category in self.cwe_files:
482+
cwe_category.seek(0)
483+
reader = csv.DictReader(cwe_category)
484+
for row in reader:
485+
yield DBWeakness(*list(row.values())[0:-1])
486+
tree = ET.parse(xml_database_path)
487+
root = tree.getroot()
488+
for tag_num in [1, 2]: # Categories , Views
489+
tag = root[tag_num]
490+
for child in tag:
491+
yield DBWeakness(
492+
*[
493+
child.attrib["ID"],
494+
child.attrib.get("Name"),
495+
None,
496+
child.attrib.get("Status"),
497+
child[0].text,
498+
]
499+
)
500+
501+
502+
Database.get_cwes = get_cwes
503+
376504

377505
class Weakness(models.Model):
378506
"""
@@ -381,7 +509,15 @@ class Weakness(models.Model):
381509

382510
cwe_id = models.IntegerField(help_text="CWE id")
383511
vulnerabilities = models.ManyToManyField(Vulnerability, related_name="weaknesses")
384-
db = Database()
512+
513+
cwe_by_id = {}
514+
515+
def get_cwe(self, cwe_id):
516+
if not self.cwe_by_id:
517+
db = Database()
518+
for weakness in db.get_cwes():
519+
self.cwe_by_id[str(weakness.cwe_id)] = weakness
520+
return self.cwe_by_id[cwe_id]
385521

386522
@property
387523
def cwe(self):
@@ -393,7 +529,7 @@ def weakness(self):
393529
Return a queryset of Weakness for this vulnerability.
394530
"""
395531
try:
396-
weakness = self.db.get(self.cwe_id)
532+
weakness = self.get_cwe(str(self.cwe_id))
397533
return weakness
398534
except Exception as e:
399535
logger.warning(f"Could not find CWE {self.cwe_id}: {e}")

vulnerabilities/pipelines/alpine_linux_importer.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ def load_advisories(
254254
affected_packages.append(
255255
AffectedPackage(
256256
package=PackageURL(
257-
type="alpine",
257+
type="apk",
258+
namespace="alpine",
258259
name=pkg_infos["name"],
259260
qualifiers=qualifiers,
260261
),
@@ -266,7 +267,8 @@ def load_advisories(
266267
affected_packages.append(
267268
AffectedPackage(
268269
package=PackageURL(
269-
type="alpine",
270+
type="apk",
271+
namespace="alpine",
270272
name=pkg_infos["name"],
271273
qualifiers=qualifiers,
272274
),

vulnerabilities/templates/index.html

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,33 @@
66
{% endblock %}
77

88
{% block content %}
9-
<section class="section pt-0">
10-
{% include "package_search_box.html" %}
11-
{% include "vulnerability_search_box.html" %}
12-
</section>
13-
{% endblock %}
9+
<section class="section pt-2">
10+
<div class="container">
11+
<div class="columns is-centered mb-5 mt-2">
12+
<div class="column is-full-tablet is-full-desktop">
13+
{% include "vulnerability_search_box.html" %}
14+
</div>
15+
</div>
16+
<div class="columns is-centered mb-5">
17+
<div class="column is-full-tablet is-full-desktop">
18+
{% include "package_search_box.html" %}
19+
</div>
20+
</div>
21+
<div class="notification is-info is-light has-text-centered">
22+
<p class="is-size-6 has-text-grey-dark has-text-centered mb-3">
23+
<strong>VulnerableCode</strong> aggregates software
24+
vulnerabilities from multiple public advisory sources
25+
and presents their details along with their affected
26+
packages and fixed-by packages identified by
27+
Package URLs (PURLs).
28+
</p>
29+
<p class="is-size-5">
30+
<strong>What's new in this Release:</strong>
31+
<a href="{{ release_url }}" target="_blank" class="has-text-link is-underlined">
32+
Check out latest updates here!
33+
</a>
34+
</p>
35+
</div>
36+
</div>
37+
</section>
38+
{% endblock %}

0 commit comments

Comments
 (0)