|
| 1 | +.. _tutorial_add_a_new_improver: |
| 2 | + |
| 3 | +Add a new improver |
| 4 | +==================== |
| 5 | + |
| 6 | +This tutorial contains all the things one should know to quickly |
| 7 | +implement an improver. |
| 8 | +A lot of internal sausage about improvers could be found inside the |
| 9 | +:file:`vulnerabilites/improver.py` file. |
| 10 | +Make sure to go through :ref:`improver-overview` before you begin writing one. |
| 11 | + |
| 12 | +TL;DR |
| 13 | +------- |
| 14 | + |
| 15 | +#. Locate the importer that this improver will be improving data of at |
| 16 | + :file:`vulnerabilities/importers/{importer_name.py}` file. |
| 17 | +#. Create a new improver subclass inheriting from the ``Improver`` superclass defined in |
| 18 | + ``vulnerabilites.improver``. It is conventional to end an improver name with *Improver*. |
| 19 | +#. Implement the ``interesting_advisories`` property to return a QuerySet of imported data |
| 20 | + (``Advisory``) you are interested in. |
| 21 | +#. Implement the ``get_inferences`` method to return an iterable of ``Inference`` objects for the |
| 22 | + given ``AdvisoryData`` |
| 23 | +#. Add the newly created improver to the improvers registry at |
| 24 | + ``vulnerabilites/improvers/__init__.py`` |
| 25 | + |
| 26 | +Prerequisites |
| 27 | +-------------- |
| 28 | + |
| 29 | +Before writing an improver, it is important to familiarize yourself with the following concepts. |
| 30 | + |
| 31 | +Importer |
| 32 | +^^^^^^^^^^ |
| 33 | + |
| 34 | +Importers are responsible for scraping vulnerability data from various data sources without creating |
| 35 | +a complete relational model between vulnerabilites, their fixes and store them in a structured |
| 36 | +fashion. These data are stored in the ``Advisory`` model and can be converted to an equivalent |
| 37 | +``AdvioryData`` for various use cases. |
| 38 | +See :ref:`importer-overview` for a brief overview on importers. |
| 39 | + |
| 40 | +Importer Prerequisites |
| 41 | +^^^^^^^^^^^^^^^^^^^^^^^ |
| 42 | + |
| 43 | +Improvers consume data produced by importers, thus it is important to familiarize yourself with |
| 44 | +:ref:`Importer Prerequisites <tutorial_add_a_new_importer_prerequisites>` |
| 45 | + |
| 46 | +Inference |
| 47 | +^^^^^^^^^^^ |
| 48 | + |
| 49 | +Inferences express the contract between the improvers and the improve runner framework. |
| 50 | +An inference is supposed to contain data points about a vulnerability without any uncertainties, |
| 51 | +which means, one inference will target one vulnerability with the specific relevant affected and |
| 52 | +fixed packages (in the form of `PackageURLs <https://github.com/package-url/packageurl-python>`_) |
| 53 | +There is no notion of version ranges here, all package versions must be explicitly specified. |
| 54 | + |
| 55 | +Because this concrete relationship is hardly available anywhere on the upstream, we have to *infer* |
| 56 | +these values, thus the name. |
| 57 | +As infering something is not always perfect, an Inference also comes with a confidence score. |
| 58 | + |
| 59 | +Improver |
| 60 | +^^^^^^^^^ |
| 61 | + |
| 62 | +All the Improvers must inherit from ``Improver`` superclass and implement the |
| 63 | +``interesting_advisories`` property and the ``get_inferences`` method. |
| 64 | + |
| 65 | +Writing an improver |
| 66 | +--------------------- |
| 67 | + |
| 68 | +Locate the Source File |
| 69 | +^^^^^^^^^^^^^^^^^^^^^^^^ |
| 70 | + |
| 71 | +If the improver will be working on data imported by an specific importer, it will sit in the same |
| 72 | +file at :file:`vulnerabilites/importers/{importer-name.py}`. |
| 73 | +Otherwise, if it is a generic improver, create a new file |
| 74 | +:file:`vulnerabilites/improvers/{improver-name.py}` |
| 75 | + |
| 76 | +Explore Package Managers (Optional) |
| 77 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 78 | + |
| 79 | +If your Improver depends on the discrete versions of a package, the package managers' VersionAPI |
| 80 | +located at :file:`vulnerabilites/package_managers.py` could come in handy. You'll need to |
| 81 | +instantiate the relevant ``VersionAPI`` in the improver's constructor and use them later in the |
| 82 | +implemented methods. See an already implemented improver (NginxBasicImprover) for an example usage. |
| 83 | + |
| 84 | +Implement the ``interesting_advisories`` Property |
| 85 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 86 | + |
| 87 | +This property is supposed to return a QuerySet of ``Advisory`` on which the ``Improver`` is |
| 88 | +interested to work on. |
| 89 | + |
| 90 | +For example, if the improver is interested to work on Advisories imported by ``ExampleImporter``, |
| 91 | +the property can be implemented as |
| 92 | + |
| 93 | +.. code-block:: python |
| 94 | +
|
| 95 | + class ExampleBasicImprover(Improver): |
| 96 | +
|
| 97 | + @property |
| 98 | + def interesting_advisories(self) -> QuerySet: |
| 99 | + return Advisory.objects.filter(created_by=ExampleImporter.qualified_name) |
| 100 | +
|
| 101 | +Implement the ``get_inferences`` Method |
| 102 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 103 | + |
| 104 | +The framework calls ``get_inferences`` method for every ``AdvisoryData`` that is obtained from |
| 105 | +the ``Advisory`` QuerySet returned by the ``interesting_advisories`` property. |
| 106 | + |
| 107 | +It is expected to return an iterable of ``Inference`` objects for the given ``AdvisoryData``. To |
| 108 | +avoid storing a lot of Inferences in memory, it is nicer to yield from this method. |
| 109 | + |
| 110 | +A very simple Improver that processes all Advisories to create the minimal relationships that can be |
| 111 | +obtained by existing data can be found at :file:`vulnerabilites/improvers/default.py` It is an |
| 112 | +example of a generic improver, for more sophisticated and targetted one, you can look at an already |
| 113 | +implemented improver (for eg, in :file:`vulnerabilites/importers/nginx.py`). |
| 114 | + |
| 115 | +Improvers are not limited to improving discrete versions, they may also improve ``aliases``. |
| 116 | +One such example, improving the importer written in the :ref:`importer tutorial |
| 117 | +<tutorial_add_a_new_importer>`, is shown below. |
| 118 | + |
| 119 | +.. code-block:: python |
| 120 | +
|
| 121 | + from datetime import datetime |
| 122 | + from datetime import timezone |
| 123 | + from typing import Iterable |
| 124 | +
|
| 125 | + import requests |
| 126 | + from django.db.models.query import QuerySet |
| 127 | + from packageurl import PackageURL |
| 128 | + from univers.version_range import NginxVersionRange |
| 129 | + from univers.versions import SemverVersion |
| 130 | +
|
| 131 | + from vulnerabilities.importer import AdvisoryData |
| 132 | + from vulnerabilities.improver import MAX_CONFIDENCE |
| 133 | + from vulnerabilities.improver import Improver |
| 134 | + from vulnerabilities.improver import Inference |
| 135 | + from vulnerabilities.models import Advisory |
| 136 | + from vulnerabilities.severity_systems import SCORING_SYSTEMS |
| 137 | +
|
| 138 | +
|
| 139 | + class ExampleImporter(Importer): |
| 140 | + ... |
| 141 | +
|
| 142 | +
|
| 143 | + class ExampleAliasImprover(Improver): |
| 144 | + @property |
| 145 | + def interesting_advisories(self) -> QuerySet: |
| 146 | + return Advisory.objects.filter(created_by=ExampleImporter.qualified_name) |
| 147 | +
|
| 148 | + def get_inferences(self, advisory_data) -> Iterable[Inference]: |
| 149 | + for alias in advisory_data.aliases: |
| 150 | + new_aliases = fetch_additional_aliases(alias) |
| 151 | + aliases = new_aliases + [alias] |
| 152 | + yield Inference(aliases=aliases, confidence=MAX_CONFIDENCE) |
| 153 | +
|
| 154 | +
|
| 155 | + def fetch_additional_aliases(alias): |
| 156 | + alias_map = { |
| 157 | + "CVE-2021-23017": ["PYSEC-1337", "CERTIN-1337"], |
| 158 | + "CVE-2021-1234": ["ANONSEC-1337", "CERTDES-1337"], |
| 159 | + } |
| 160 | + return alias_map.get(alias) |
| 161 | +
|
| 162 | +
|
| 163 | +.. note:: |
| 164 | + |
| 165 | + | Use ``make valid`` to format your new code using black and isort automatically. |
| 166 | + | Use ``make check`` to check for formatting errrors. |
| 167 | +
|
| 168 | +Register the Improver |
| 169 | +^^^^^^^^^^^^^^^^^^^^^^ |
| 170 | + |
| 171 | +Finally, register your improver in the improver registry at |
| 172 | +:file:`vulnerabilites/improvers/__init__.py` |
| 173 | + |
| 174 | +.. code-block:: python |
| 175 | + :emphasize-lines: 7 |
| 176 | +
|
| 177 | + from vulnerabilities import importers |
| 178 | + from vulnerabilities.improvers import default |
| 179 | +
|
| 180 | + IMPROVERS_REGISTRY = [ |
| 181 | + default.DefaultImprover, |
| 182 | + importers.nginx.NginxBasicImprover, |
| 183 | + importers.example.ExampleAliasImprover, |
| 184 | + ] |
| 185 | +
|
| 186 | + IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY} |
| 187 | +
|
| 188 | +Congratulations! You've written your first improver. |
| 189 | + |
| 190 | +Run Your First Improver |
| 191 | +^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 192 | + |
| 193 | +If everything went fine, you'll see your improver in the list of available improvers |
| 194 | + |
| 195 | +.. code-block:: console |
| 196 | + :emphasize-lines: 6 |
| 197 | +
|
| 198 | + $ ./manage.py improve --list |
| 199 | +
|
| 200 | + Vulnerability data can be processed by these available improvers: |
| 201 | + vulnerabilities.improvers.default.DefaultImprover |
| 202 | + vulnerabilities.importers.nginx.NginxBasicImprover |
| 203 | + vulnerabilities.importers.example.ExampleAliasImprover |
| 204 | +
|
| 205 | +Before running the improver, make sure you have imported the data. An improver cannot improve if |
| 206 | +there is nothing imported. |
| 207 | + |
| 208 | +.. code-block:: console |
| 209 | +
|
| 210 | + $ ./manage.py import vulnerabilities.importers.example.ExampleImporter |
| 211 | +
|
| 212 | + Importing data using vulnerabilities.importers.example.ExampleImporter |
| 213 | + Successfully imported data using vulnerabilities.importers.example.ExampleImporter |
| 214 | +
|
| 215 | +Now, run the improver |
| 216 | + |
| 217 | +.. code-block:: console |
| 218 | +
|
| 219 | + $ ./manage.py improve vulnerabilities.importers.example.ExampleAliasImprover |
| 220 | +
|
| 221 | + Improving data using vulnerabilities.importers.example.ExampleAliasImprover |
| 222 | + Successfully improved data using vulnerabilities.importers.example.ExampleAliasImprover |
| 223 | +
|
| 224 | +See :ref:`command_line_interface` for command line usage instructions. |
| 225 | + |
| 226 | +Enable Debug Logging (Optional) |
| 227 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 228 | + |
| 229 | +For more visibility, turn on debug logs in :file:`vulnerablecode/settings.py`. |
| 230 | + |
| 231 | +.. code-block:: python |
| 232 | +
|
| 233 | + DEBUG = True |
| 234 | + LOGGING = { |
| 235 | + 'version': 1, |
| 236 | + 'disable_existing_loggers': False, |
| 237 | + 'handlers': { |
| 238 | + 'console': { |
| 239 | + 'class': 'logging.StreamHandler', |
| 240 | + }, |
| 241 | + }, |
| 242 | + 'root': { |
| 243 | + 'handlers': ['console'], |
| 244 | + 'level': 'DEBUG', |
| 245 | + }, |
| 246 | + } |
| 247 | +
|
| 248 | +Invoke the improve command now and you'll see (in a fresh database, after importing) |
| 249 | + |
| 250 | +.. code-block:: console |
| 251 | +
|
| 252 | + $ ./manage.py improve vulnerabilities.importers.example.ExampleAliasImprover |
| 253 | +
|
| 254 | + Improving data using vulnerabilities.importers.example.ExampleAliasImprover |
| 255 | + Running improver: vulnerabilities.importers.example.ExampleAliasImprover |
| 256 | + Improving advisory id: 1 |
| 257 | + New alias for <Vulnerability: VULCOID-23dd9060-3bc0-4454-bfbd-d16c08a966a6>: PYSEC-1337 |
| 258 | + New alias for <Vulnerability: VULCOID-23dd9060-3bc0-4454-bfbd-d16c08a966a6>: CVE-2021-23017 |
| 259 | + New alias for <Vulnerability: VULCOID-23dd9060-3bc0-4454-bfbd-d16c08a966a6>: CERTIN-1337 |
| 260 | + Improving advisory id: 2 |
| 261 | + New alias for <Vulnerability: VULCOID-fae4e06e-4815-45fe-ae95-8d2356ffb5b9>: CERTDES-1337 |
| 262 | + New alias for <Vulnerability: VULCOID-fae4e06e-4815-45fe-ae95-8d2356ffb5b9>: ANONSEC-1337 |
| 263 | + New alias for <Vulnerability: VULCOID-fae4e06e-4815-45fe-ae95-8d2356ffb5b9>: CVE-2021-1234 |
| 264 | + Finished improving using vulnerabilities.importers.example.ExampleAliasImprover. |
| 265 | + Successfully improved data using vulnerabilities.importers.example.ExampleAliasImprover |
| 266 | +
|
| 267 | +.. note:: |
| 268 | + |
| 269 | + Even though CVE-2021-23017 and CVE-2021-1234 are not supplied by this improver yet it shows them |
| 270 | + because we left out running the ``DefaultImprover`` in the example. The ``DefaultImprover`` |
| 271 | + inserts minimal data found via the importers in the database (Here, the above two CVEs). Run |
| 272 | + importer, DefaultImprover and then your improver in this sequence to avoid this anomaly. |
0 commit comments