Skip to content

Commit

Permalink
NIST CVE SYNC JOB CREATED
Browse files Browse the repository at this point in the history
Created job that looks up current software in the DLC, finds/pulls/creates/associates CVEs for the software and updates existing ones if necessary.
Ugraded to minor revision 1.1.0
Validated against 1.1.6 and 1.3.3
  • Loading branch information
Brandon Minnix committed May 26, 2022
1 parent b6c91ee commit 31c7584
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 9 deletions.
2 changes: 1 addition & 1 deletion development/nautobot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def is_truthy(arg):
"nautobot_device_lifecycle_mgmt": {
"barchart_bar_width": float(os.environ.get("BARCHART_BAR_WIDTH", 0.1)),
"barchart_width": int(os.environ.get("BARCHART_WIDTH", 12)),
"barchart_height": int(os.environ.get("BARCHART_HEIGHT", 5)),
"barchart_height": int(os.environ.get("BARCHART_HEIGHT", 5))
},
}

Expand Down
1 change: 1 addition & 0 deletions nautobot_device_lifecycle_mgmt/api/nested_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class Meta:
"display",
"name",
"published_date",
"last_modified_date",
"link",
"status",
"description",
Expand Down
1 change: 1 addition & 0 deletions nautobot_device_lifecycle_mgmt/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ class Meta:
"url",
"name",
"published_date",
"last_modified_date",
"link",
"status",
"description",
Expand Down
4 changes: 4 additions & 0 deletions nautobot_device_lifecycle_mgmt/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,10 @@ class CVELCMFilterSet(StatusModelFilterSetMixin, CustomFieldModelFilterSet):
published_date__gte = django_filters.DateFilter(field_name="published_date", lookup_expr="gte")
published_date__lte = django_filters.DateFilter(field_name="published_date", lookup_expr="lte")

last_modified_date = django_filters.DateTimeFromToRangeFilter()
last_modified_date__gte = django_filters.DateFilter(field_name="last_modified_date", lookup_expr="gte")
last_modified_date__lte = django_filters.DateFilter(field_name="last_modified_date", lookup_expr="lte")

cvss__gte = django_filters.NumberFilter(field_name="cvss", lookup_expr="gte")
cvss__lte = django_filters.NumberFilter(field_name="cvss", lookup_expr="lte")

Expand Down
7 changes: 7 additions & 0 deletions nautobot_device_lifecycle_mgmt/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@ class CVELCMForm(
"""CVE Lifecycle Management creation/edit form."""

published_date = forms.DateField(widget=DatePicker())
last_modified_date = forms.DateField(widget=DatePicker(), required=False)
severity = forms.ChoiceField(choices=CVESeverityChoices.CHOICES, label="Severity", required=False)
tags = DynamicModelMultipleChoiceField(queryset=Tag.objects.all(), required=False)

Expand All @@ -1002,6 +1003,7 @@ class Meta:

widgets = {
"published_date": DatePicker(),
"last_modified_date": DatePicker(),
}


Expand Down Expand Up @@ -1043,6 +1045,9 @@ class CVELCMFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterF
published_date_before = forms.DateField(label="Published Date Before", required=False, widget=DatePicker())
published_date_after = forms.DateField(label="Published Date After", required=False, widget=DatePicker())

last_modified_date_before = forms.DateField(label="Last Modified Date Before", required=False, widget=DatePicker())
last_modified_date_after = forms.DateField(label="Last Modified Date After", required=False, widget=DatePicker())

cvss__gte = forms.FloatField(label="CVSS Score Above", required=False)
cvss__lte = forms.FloatField(label="CVSS Score Below", required=False)

Expand Down Expand Up @@ -1070,6 +1075,8 @@ class Meta:
"q",
"published_date_before",
"published_date_after",
"last_modified_date_before",
"last_modified_date_after",
"severity",
"status",
]
Expand Down
4 changes: 2 additions & 2 deletions nautobot_device_lifecycle_mgmt/jobs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Nautobot Jobs for the Device Lifecycle plugin."""
from .cve_tracking import GenerateVulnerabilities
from .cve_tracking import GenerateVulnerabilities, NistCveSyncSoftware
from .lifecycle_reporting import DeviceSoftwareValidationFullReport, InventoryItemSoftwareValidationFullReport

jobs = [DeviceSoftwareValidationFullReport, InventoryItemSoftwareValidationFullReport, GenerateVulnerabilities]
jobs = [DeviceSoftwareValidationFullReport, InventoryItemSoftwareValidationFullReport, GenerateVulnerabilities, NistCveSyncSoftware]
242 changes: 241 additions & 1 deletion nautobot_device_lifecycle_mgmt/jobs/cve_tracking.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"""Jobs for the CVE Tracking portion of the Device Lifecycle plugin."""
from datetime import datetime
from datetime import datetime, date
import dateutil
import json

from nautobot.extras.jobs import Job, StringVar
from nautobot.extras.models import Relationship, RelationshipAssociation

from nautobot_device_lifecycle_mgmt.models import (
CVELCM,
SoftwareLCM,
VulnerabilityLCM,
)

from nautobot_device_lifecycle_mgmt.software import ItemSoftware

import requests
import re
from time import sleep

name = "CVE Tracking" # pylint: disable=invalid-name

Expand Down Expand Up @@ -65,3 +73,235 @@ def run(self, data, commit): # pylint: disable=too-many-locals

diff = VulnerabilityLCM.objects.count() - count_before
self.log_success(message=f"Processed {cves.count()} CVEs and generated {diff} Vulnerabilities.")


class NistCveSyncSoftware(Job):
"""Checks all software in the DLC Plugin for \
NIST recorded vulnerabilities
"""
name = "Find current NIST CVE for Software in Database"
description = """Searches the NIST DBs for CVEs \
related to software"""
read_only = False

class Meta: # pylint: disable=too-few-public-methods
"""Meta class for the job."""

commit_default = True

def run(self, data, commit):
"""Check all software in DLC against NIST database and \
associate registered CVEs. Update when necessary.
"""
all_software = SoftwareLCM.objects.all()

all_cves = {}
cve_counter = 0
update_counter = 0
for software in all_software:
manufacturer = str(software.device_platform.manufacturer).lower()
platform = str(software.device_platform.name).split(" ",1)[1].lower()
platform = platform.replace(" ","_")
version = str(software.version)

cpe_software_search_url = self.create_cpe_software_search_url(
manufacturer,
platform,
version
)

software_cve_info = self.get_cve_info(
cpe_software_search_url,
software.id
)

cve_counter += len(software_cve_info)
create_new_cves = self.create_dlc_cves(
software.id,
software_cve_info
)

self.log_success(
message=f"""Performed discovery on all software meeting \
naming standards. Added {cve_counter} CVE."""
)
self.update_cves()


def create_cpe_software_search_url(
self, manufacturer: str, platform: str, version: str
) -> str:
"""Convert the data into the url for a cpe search against the \
NIST DB"""
escape_list = [r"\(", r"\)"]
base_url = f"""https://services.nvd.nist.gov/rest/json/cpes/1.0?addOns=cves&cpeMatchString=cpe:2.3:*:"""
version = version

for escape_char in escape_list:
if re.search(escape_char, version):
version = re.sub(escape_char, "\\"+escape_char, version)

extended_url = f'{manufacturer}:{platform}:{version}:*:*:*:*:*:*:*'
return f"{base_url}{extended_url}"

def prep_cve_for_dlc(self, url):
cve_name = url.split('/')[-1]
cve_search_url = f"{url}"
result = json.loads(requests.get(cve_search_url).text)

if result.get('message'):
self.log_info(
message=f"""CVE {cve_name} DOES NOT EXIST IN NIST DATABASE"""
)
return

cve_base = result['result']['CVE_Items'][0]
cve_description = cve_base['cve']['description']['description_data']\
[0]['value']
cve_published_date = cve_base.get('publishedDate')
cve_modified_date = cve_base.get('lastModifiedDate')
cve_impact = cve_base.get('impact')

## Determine URL
if len(result['result']['CVE_Items'][0]['cve']['references']\
['reference_data']) > 0:
cve_url = result['result']['CVE_Items'][0]['cve']['references']\
['reference_data'][0].get(
'url',
f"https://www.cvedetails.com/cve/{cve_name}/"
)
else:
cve_url = f"https://www.cvedetails.com/cve/{cve_name}/"



if cve_impact.get('baseMetricV3'):
cvss_base_score = cve_impact['baseMetricV3']['cvssV3']['baseScore']
cvss_severity = cve_impact['baseMetricV3']['cvssV3']['baseSeverity']
cvssv2_score = cve_impact['baseMetricV2']['exploitabilityScore']
cvssv3_score = cve_impact['baseMetricV3']['exploitabilityScore']
else:
cvss_base_score = cve_impact['baseMetricV2']['cvssV2']['baseScore']
cvss_severity = cve_impact['baseMetricV2']['severity']
cvssv2_score = cve_impact['baseMetricV2']['exploitabilityScore']
cvssv3_score = None

all_cve_info = {
'url': cve_url,
'description': cve_description,
'published_date': cve_published_date,
'modified_date': cve_modified_date,
'cvss_base_score': cvss_base_score,
'cvss_severity': cvss_severity,
'cvssv2_score': cvssv2_score,
'cvssv3_score': cvssv3_score
}

return all_cve_info

def get_cve_info(self, cpe_software_search_url: str, software_id=None) -> dict:
"""Search NIST for software and related CVEs"""
cpe_info = json.loads(requests.get(cpe_software_search_url).text)
all_cve_info = {}
if len(cpe_info['result']['cpes']) > 0:
cve_list = cpe_info['result']['cpes'][0].get('vulnerabilities', [])
base_url = "https://services.nvd.nist.gov/rest/json/cve/1.0/"

dlc_cves = [cve.name for cve in CVELCM.objects.all()]

if len(cve_list) > 0:
for cve in cve_list:
if cve not in dlc_cves:
if re.search("^CVE", cve):
all_cve_info[cve] = self.prep_cve_for_dlc(base_url+cve)
sleep(.25)
else:
existing_cve = CVELCM.objects.get(name=cve)
self.associate_software_to_cve(software_id, existing_cve.id)

return all_cve_info

def create_dlc_cves(self, software_id, cpe_cves):
"""Create the list of items that will need to be inserted to DLC CVEs"""
dlc_cves = CVELCM.objects.all()
for cve, info in cpe_cves.items():
self.log_info(message=f"{date.fromisoformat(info['published_date'][0:10])}")
if cve not in dlc_cves:
create_cves = CVELCM.objects.get_or_create(
name=cve,
description=(
f"{info['description'][0:251]}..." \
if len(info['description']) > \
255 else info['description']),
published_date=date.fromisoformat(info['published_date'][0:10]),
last_modified_date=date.fromisoformat(info['modified_date'][0:10]),
link=info['url'],
cvss=info['cvss_base_score'],
severity=info['cvss_severity'],
cvss_v2=info['cvssv2_score'],
cvss_v3=info['cvssv3_score'],
comments="ENTRY CREATED BY NAUTOBOT NIST JOB"
)

self.log_info(message=f"""Created {cve}""")
cve = CVELCM.objects.get(name=cve)

self.associate_software_to_cve(software_id, cve.id)
else:
cve = CVELCM.objects.get(name=cve)
self.associate_software_to_cve(software_id, cve)

def associate_software_to_cve(self, software_id, cve):
"""A method to associate the software to the CVE"""
r_type = Relationship.objects.get(slug="soft_cve")
RelationshipAssociation.objects.get_or_create(
relationship_id=r_type.id,
source_type_id=r_type.source_type_id,
source_id=software_id,
destination_type_id=r_type.destination_type_id,
destination_id=cve
)

def update_cves(self):
"""A method to ensure the CVE in DLC is the latest version"""
self.log_info(message=f"""Checking for CVE Modifications""")
base_url = "https://services.nvd.nist.gov/rest/json/cve/1.0/"
dlc_cves = CVELCM.objects.all()

for cve in dlc_cves:
try:
result = self.prep_cve_for_dlc(base_url+cve.name)

if str(result.get('modified_date')[0:10]) \
!= str(cve.last_modified_date):
cve.description = (f"{result['description'][0:251]}..." \
if len(result['description']) > \
255 else result['description'])
cve.last_modified_date=\
f"{result.get('modified_date')[0:10]}"
cve.link = result['url']
cve.cvss = result['cvss_base_score']
cve.severity = result['cvss_severity']
cve.cvss_v2 = result['cvssv2_score']
cve.cvss_v3 = result['cvssv3_score']
cve.comments = "ENTRY UPDATED BY NAUTOBOT NIST JOB"

try:
cve.validated_save()
self.log_info(
message=f"""{cve.name} was modified."""
)

except:
self.log_info(
message=f"""Unable to update {cve.name}."""
)
pass

except:
pass

self.log_success(
message=f"""All CVE's requiring modifications have been updated."""
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.10 on 2022-05-12 19:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('nautobot_device_lifecycle_mgmt', '0009_software_remove_image_fields'),
]

operations = [
migrations.AddField(
model_name='cvelcm',
name='last_modified_date',
field=models.DateField(blank=True, null=True),
),
]
7 changes: 7 additions & 0 deletions nautobot_device_lifecycle_mgmt/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,11 @@ class CVELCM(PrimaryModel):

name = models.CharField(max_length=16, blank=False, unique=True)
published_date = models.DateField(verbose_name="Published Date")
last_modified_date = models.DateField(
null=True,
blank=True,
verbose_name="Last Modified Date"
)
link = models.URLField()
status = StatusField(
null=True,
Expand All @@ -765,6 +770,7 @@ class CVELCM(PrimaryModel):
csv_headers = [
"name",
"published_date",
"last_modified_date",
"link",
"status",
"description",
Expand Down Expand Up @@ -796,6 +802,7 @@ def to_csv(self):
return (
self.name,
self.published_date,
self.last_modified_date,
self.link,
self.status,
self.description,
Expand Down
Loading

0 comments on commit 31c7584

Please sign in to comment.