Skip to content

Commit 925d0ef

Browse files
committed
Implement improver
This is work in progress, there are a few bugs and a few fixmes as well. Everything will be replaced before the final commit Signed-off-by: Hritik Vijay <hritikxx8@gmail.com>
1 parent ae79a86 commit 925d0ef

9 files changed

Lines changed: 220 additions & 102 deletions

File tree

vulnerabilities/data_inference.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ def __post_init__(self):
2727

2828
if self.confidence < 0:
2929
raise UnderConfidenceError
30+
31+
class Improver:
32+
"""
33+
All improvers should inherit this class and implement updated_inferences method to return
34+
new inferences for a package or vulnerability
35+
"""
36+
def updated_inferences(self):
37+
raise NotImplementedError

vulnerabilities/data_source.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ class Advisory:
8686

8787
summary: str
8888
vulnerability_id: Optional[str] = None
89-
affected_packages: List[AffectedPackage] = dataclasses.field(default_factory=list)
89+
affected_package_urls: Iterable[PackageURL] = dataclasses.field(default_factory=list)
90+
fixed_package_urls: Iterable[PackageURL] = dataclasses.field(default_factory=list)
9091
references: List[Reference] = dataclasses.field(default_factory=list)
9192

9293
def __post_init__(self):
9394
if self.vulnerability_id and not is_cve(self.vulnerability_id):
9495
raise ValueError("CVE expected, found: {}".format(self.vulnerability_id))
9596

9697
def normalized(self):
98+
affected_package_urls = set(self.affected_package_urls)
99+
fixed_package_urls = set(self.fixed_package_urls)
97100
references = sorted(
98101
self.references, key=lambda reference: (reference.reference_id, reference.url)
99102
)
@@ -103,7 +106,8 @@ def normalized(self):
103106
return Advisory(
104107
summary=self.summary,
105108
vulnerability_id=self.vulnerability_id,
106-
affected_packages=sorted(self.affected_packages),
109+
affected_package_urls=affected_package_urls,
110+
fixed_package_urls=fixed_package_urls,
107111
references=references,
108112
)
109113

vulnerabilities/import_runner.py

Lines changed: 12 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
import dataclasses
2525
import datetime
2626
import logging
27-
from itertools import chain
2827
from typing import Tuple
2928
from typing import Set
3029

31-
from django.db import transaction
3230

3331
from vulnerabilities import models
34-
from vulnerabilities.data_source import Advisory, DataSource
32+
from vulnerabilities.data_source import Advisory
3533
from vulnerabilities.data_source import PackageURL
34+
from vulnerabilities.data_inference import Inference
35+
from vulnerabilities.data_inference import MAX_CONFIDENCE
36+
from vulnerabilities.improve_runner import process_inferences
3637

3738
logger = logging.getLogger(__name__)
3839

@@ -87,7 +88,7 @@ def run(self, cutoff_date: datetime.datetime = None) -> None:
8788
data_source = self.importer.make_data_source(cutoff_date=cutoff_date)
8889
with data_source:
8990
advisories = data_source.updated_advisories()
90-
process_advisories(advisories)
91+
process_advisories("importer", advisories)
9192
self.importer.last_run = datetime.datetime.now(tz=datetime.timezone.utc)
9293
self.importer.data_source_cfg = dataclasses.asdict(data_source.config)
9394
self.importer.save()
@@ -108,79 +109,10 @@ def get_vuln_pkg_refs(vulnerability, package):
108109
)
109110

110111

111-
@transaction.atomic
112-
def process_advisories(advisories: Set[Advisory]) -> None:
113-
bulk_create_vuln_pkg_refs = set()
114-
for advisory in advisories:
115-
vuln, vuln_created = _get_or_create_vulnerability(advisory)
116-
for vuln_ref in advisory.references:
117-
ref, _ = models.VulnerabilityReference.objects.get_or_create(
118-
vulnerability=vuln, reference_id=vuln_ref.reference_id, url=vuln_ref.url
119-
)
120-
121-
for score in vuln_ref.severities:
122-
models.VulnerabilitySeverity.objects.update_or_create(
123-
vulnerability=vuln,
124-
scoring_system=score.system.identifier,
125-
reference=ref,
126-
defaults={"value": str(score.value)},
127-
)
128-
129-
for aff_pkg_with_patched_pkg in advisory.affected_packages:
130-
vulnerable_package, _ = _get_or_create_package(
131-
aff_pkg_with_patched_pkg.vulnerable_package
132-
)
133-
patched_package = None
134-
if aff_pkg_with_patched_pkg.patched_package:
135-
patched_package, _ = _get_or_create_package(
136-
aff_pkg_with_patched_pkg.patched_package
137-
)
138-
139-
prv, _ = models.PackageRelatedVulnerability.objects.get_or_create(
140-
vulnerability=vuln,
141-
package=vulnerable_package,
142-
)
143-
144-
if patched_package:
145-
prv.patched_package = patched_package
146-
prv.save()
147-
148-
models.PackageRelatedVulnerability.objects.bulk_create(
149-
[i.to_model_object() for i in bulk_create_vuln_pkg_refs]
150-
)
151-
152-
153-
def _get_or_create_vulnerability(
154-
advisory: Advisory,
155-
) -> Tuple[models.Vulnerability, bool]:
156-
157-
vuln, created = models.Vulnerability.objects.get_or_create(
158-
vulnerability_id=advisory.vulnerability_id
159-
) # nopep8
160-
# Eventually we only want to keep summary from NVD and ignore other descriptions.
161-
if advisory.summary and vuln.summary != advisory.summary:
162-
vuln.summary = advisory.summary
163-
vuln.save()
164-
165-
return vuln, created
166-
167-
168-
def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
169-
170-
query_kwargs = {}
171-
for key, val in p.to_dict().items():
172-
if not val:
173-
if key == "qualifiers":
174-
query_kwargs[key] = {}
175-
else:
176-
query_kwargs[key] = ""
177-
else:
178-
query_kwargs[key] = val
179-
180-
return models.Package.objects.get_or_create(**query_kwargs)
181-
182-
183-
def _package_url_to_package(purl: PackageURL) -> models.Package:
184-
p = models.Package()
185-
p.set_package_url(purl)
186-
return p
112+
def process_advisories(source: str, advisories: Set[Advisory]) -> None:
113+
"""
114+
Insert advisories into the database
115+
Advisories are treated as full confidence infererences.
116+
"""
117+
inferences = [ Inference(advisory, source, MAX_CONFIDENCE) for advisory in advisories ]
118+
process_inferences(inferences)

vulnerabilities/importers/nginx.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from vulnerabilities.package_managers import GitHubTagsAPI
3737
from vulnerabilities.package_managers import Version
3838
from vulnerabilities.helpers import nearest_patched_package
39+
from vulnerabilities.helpers import AffectedPackage
3940

4041

4142
@dataclasses.dataclass
@@ -117,7 +118,8 @@ def to_advisories(self, data):
117118
Advisory(
118119
vulnerability_id=cve_id,
119120
summary=summary,
120-
affected_packages=nearest_patched_package(vulnerable_packages, fixed_packages),
121+
affected_package_urls=vulnerable_packages,
122+
fixed_package_urls=fixed_packages,
121123
)
122124
)
123125

vulnerabilities/improve_runner.py

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from datetime import datetime
22
import dataclasses
33
import logging
4+
from typing import Tuple
5+
6+
from django.db import transaction
7+
8+
from vulnerabilities import models
9+
from vulnerabilities.data_source import PackageURL
10+
from vulnerabilities.data_source import Advisory
411

512
logger = logging.getLogger(__name__)
613

@@ -15,11 +22,111 @@ def __init__(self, improver):
1522
self.improver = improver
1623

1724
def run(self) -> None:
18-
logger.info("Improving using %s.", self.improver.__module__)
19-
inferences = self.improver.updated_inferences()
25+
logger.info("Improving using %s.", self.improver.__name__)
26+
inferences = self.improver().updated_inferences()
2027
process_inferences(inferences)
21-
logger.info("Finished improving using %s.", self.improver.__module__)
28+
logger.info("Finished improving using %s.", self.improver.__name__)
2229

2330

31+
@transaction.atomic
2432
def process_inferences(inferences):
25-
...
33+
bulk_create_vuln_pkg_refs = set()
34+
for inference in inferences:
35+
advisory = inference.advisory
36+
vuln, vuln_created = _get_or_create_vulnerability(advisory)
37+
for vuln_ref in advisory.references:
38+
ref, _ = models.VulnerabilityReference.objects.get_or_create(
39+
vulnerability=vuln, reference_id=vuln_ref.reference_id, url=vuln_ref.url
40+
)
41+
42+
for score in vuln_ref.severities:
43+
models.VulnerabilitySeverity.objects.update_or_create(
44+
vulnerability=vuln,
45+
scoring_system=score.system.identifier,
46+
reference=ref,
47+
defaults={"value": str(score.value)},
48+
)
49+
50+
for aff_pkg in advisory.affected_package_urls:
51+
vulnerable_package, _ = _get_or_create_package(
52+
aff_pkg
53+
)
54+
create_or_update_relation(
55+
relation=models.PackageRelatedVulnerability,
56+
vulnerability=vuln,
57+
source=inference.source,
58+
package=vulnerable_package,
59+
confidence=inference.confidence)
60+
61+
for fixed_pkg in advisory.fixed_package_urls:
62+
patched_package, _ = _get_or_create_package(
63+
fixed_pkg
64+
)
65+
create_or_update_relation(
66+
relation=models.PackageRelatedVulnerabilityFix,
67+
vulnerability=vuln,
68+
source=inference.source,
69+
package=vulnerable_package,
70+
confidence=inference.confidence)
71+
72+
73+
models.PackageRelatedVulnerability.objects.bulk_create(
74+
[i.to_model_object() for i in bulk_create_vuln_pkg_refs]
75+
)
76+
77+
78+
def _get_or_create_vulnerability(
79+
advisory: Advisory,
80+
) -> Tuple[models.Vulnerability, bool]:
81+
82+
vuln, created = models.Vulnerability.objects.get_or_create(
83+
vulnerability_id=advisory.vulnerability_id
84+
) # nopep8
85+
# Eventually we only want to keep summary from NVD and ignore other descriptions.
86+
if advisory.summary and vuln.summary != advisory.summary:
87+
vuln.summary = advisory.summary
88+
vuln.save()
89+
90+
return vuln, created
91+
92+
93+
def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
94+
95+
query_kwargs = {}
96+
for key, val in p.to_dict().items():
97+
if not val:
98+
if key == "qualifiers":
99+
query_kwargs[key] = {}
100+
else:
101+
query_kwargs[key] = ""
102+
else:
103+
query_kwargs[key] = val
104+
105+
return models.Package.objects.get_or_create(**query_kwargs)
106+
107+
108+
def _package_url_to_package(purl: PackageURL) -> models.Package:
109+
p = models.Package()
110+
p.set_package_url(purl)
111+
return p
112+
113+
def create_or_update_relation(relation, vulnerability, source, package, confidence):
114+
try:
115+
entry = relation.objects.get(
116+
vulnerability=vulnerability,
117+
package=package
118+
)
119+
if confidence > entry.confidence:
120+
entry.source = source
121+
entry.confidence = confidence
122+
entry.save()
123+
logger.debug("%s: Confidence improved for %s R %s, new confidence: %d", relation, package, vulnerability, confidence)
124+
125+
except relation.DoesNotExist:
126+
relation.objects.create(
127+
vulnerability=vulnerability,
128+
source=source,
129+
package=package,
130+
confidence=confidence
131+
)
132+
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
IMPROVER_REGISTRY = []
1+
from . import nginx
22

3-
def class_name(module_name: str):
3+
IMPROVER_REGISTRY = [nginx.NginxTimeTravel]
4+
5+
def find_class(class_name: str):
6+
# FIXME: this might cause problems when there are two modules containing same class name, think of a better approach
47
for improver in IMPROVER_REGISTRY:
5-
if improver.__module__ == module_name:
8+
if class_name == improver.__name__:
69
return improver
710

811
raise AttributeError

vulnerabilities/improvers/nginx.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from packageurl import PackageURL
2+
3+
from vulnerabilities.data_inference import Improver
4+
from vulnerabilities.data_inference import Advisory
5+
from vulnerabilities.data_inference import Inference
6+
from vulnerabilities.helpers import nearest_patched_package
7+
from vulnerabilities.models import Vulnerability
8+
from vulnerabilities.models import Package
9+
10+
class NginxTimeTravel(Improver):
11+
def updated_inferences(self):
12+
inferences = []
13+
14+
vulnerabilities = set(Vulnerability.objects.filter(vulnerable_packages__name="nginx"))
15+
vulnerabilities.union(Vulnerability.objects.filter(patched_packages__name="nginx"))
16+
17+
for vulnerability in vulnerabilities:
18+
affected_packages = map(package_url, Package.objects.filter(vulnerable_package__package__name="nginx", vulnerabilities = vulnerability))
19+
fixed_packages = map(package_url, Package.objects.filter(patched_package__package__name="nginx", vulnerabilities = vulnerability))
20+
21+
time_traveller = nearest_patched_package(affected_packages, fixed_packages)
22+
affected_packages = [ affected_package.vulnerable_package for affected_package in time_traveller]
23+
fixed_packages = [ affected_package.patched_package for affected_package in time_traveller if affected_package.patched_package is not None]
24+
25+
inference = Inference(advisory = Advisory(
26+
vulnerability_id=vulnerability.vulnerability_id,
27+
summary=vulnerability.summary,
28+
affected_package_urls=fixed_packages,
29+
), source="time travel", confidence=30)
30+
inferences.append(inference)
31+
32+
return inferences
33+
34+
35+
def package_url(package):
36+
return PackageURL(
37+
type=package.type,
38+
namespace=package.namespace,
39+
name=package.name,
40+
version=package.version,
41+
subpath=package.subpath,
42+
qualifiers=package.qualifiers
43+
)
44+

0 commit comments

Comments
 (0)