launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28389
[Merge] ~lgp171188/launchpad:vulnerability-creation-api into launchpad:master
Guruprasad has proposed merging ~lgp171188/launchpad:vulnerability-creation-api into launchpad:master.
Commit message:
WIP changes
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/420401
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:vulnerability-creation-api into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 9f981c0..1d3cb17 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -47,6 +47,7 @@ from lp.bugs.interfaces.structuralsubscription import (
IStructuralSubscription,
IStructuralSubscriptionTarget,
)
+from lp.bugs.interfaces.vulnerability import IVulnerability
from lp.buildmaster.interfaces.builder import (
IBuilder,
IBuilderSet,
@@ -452,6 +453,7 @@ patch_collection_property(IDistribution, 'all_distro_archives', IArchive)
patch_entry_return_type(IDistribution, 'newOCIProject', IOCIProject)
patch_collection_return_type(
IDistribution, 'searchOCIProjects', IOCIProject)
+patch_collection_property(IDistribution, 'vulnerabilities', IVulnerability)
# IDistributionMirror
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index 142d99a..d03a00c 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -892,6 +892,11 @@
permission="zope.Public"
template="../templates/cve-portlet-bugs2.pt"/>
<browser:url
+ for="lp.bugs.interfaces.vulnerability.IVulnerability"
+ path_expression="string:+vulnerability/${id}"
+ attribute_to_parent="distribution"
+ rootsite="bugs"/>
+ <browser:url
for="lp.bugs.interfaces.bugsubscription.IBugSubscription"
path_expression="string:+subscription/${person/name}"
attribute_to_parent="bug"
diff --git a/lib/lp/bugs/enums.py b/lib/lp/bugs/enums.py
index 913e1bf..e6f6fc4 100644
--- a/lib/lp/bugs/enums.py
+++ b/lib/lp/bugs/enums.py
@@ -8,6 +8,7 @@ __all__ = [
'BugLockStatus',
'BugNotificationLevel',
'BugNotificationStatus',
+ 'VulnerabilityStatus',
]
from lazr.enum import (
@@ -103,3 +104,31 @@ class BugLockedStatus(DBEnumeratedType):
The various locked status values of a bug.
"""
use_template(BugLockStatus, exclude=('UNLOCKED',))
+
+
+class VulnerabilityStatus(DBEnumeratedType):
+ """Vulnerability status"""
+
+ NEEDS_TRIAGE = DBItem(0, """
+ Needs triage
+
+ Not looked at yet.
+ """)
+
+ ACTIVE = DBItem(1, """
+ Active
+
+ The vulnerability is active.
+ """)
+
+ IGNORED = DBItem(2, """
+ Ignored
+
+ The vulnerability is currently ignored.
+ """)
+
+ RETIRED = DBItem(3, """
+ Retired
+
+ This vulnerability is now retired.
+ """)
diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
index 8fa01da..0d0b8ad 100644
--- a/lib/lp/bugs/interfaces/vulnerability.py
+++ b/lib/lp/bugs/interfaces/vulnerability.py
@@ -9,13 +9,16 @@ __all__ = [
'IVulnerabilityActivitySet',
'IVulnerabilitySet',
'VulnerabilityChange',
- 'VulnerabilityStatus'
]
from lazr.enum import (
DBEnumeratedType,
DBItem,
)
+from lazr.restful.declarations import (
+ exported,
+ exported_as_webservice_entry,
+ )
from lazr.restful.fields import Reference
from zope.interface import Interface
from zope.schema import (
@@ -28,6 +31,7 @@ from zope.schema import (
from lp import _
from lp.app.enums import InformationType
from lp.app.interfaces.informationtype import IInformationType
+from lp.bugs.enums import VulnerabilityStatus
from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.bugs.interfaces.cve import ICve
from lp.registry.interfaces.distribution import IDistribution
@@ -84,52 +88,53 @@ class VulnerabilityChange(DBEnumeratedType):
""")
-class VulnerabilityStatus(DBEnumeratedType):
- """Vulnerability status"""
-
- NEEDS_TRIAGE = DBItem(0, """
- Needs triage
-
- Not looked at yet.
- """)
-
- ACTIVE = DBItem(1, """
- Active
-
- The vulnerability is active.
- """)
-
- IGNORED = DBItem(2, """
- Ignored
-
- The vulnerability is currently ignored.
- """)
-
- RETIRED = DBItem(3, """
- Retired
-
- This vulnerability is now retired.
- """)
-
-
class IVulnerabilityView(Interface):
"""`IVulnerability` attributes that require launchpad.View."""
- id = Int(title=_("ID"), required=True, readonly=True)
+ id = exported(
+ Int(title=_("ID"), required=True, readonly=True),
+ as_of="devel"
+ )
- distribution = Reference(IDistribution, title=_("Distribution"),
- required=True, readonly=True)
+ distribution = exported(
+ Reference(
+ IDistribution,
+ title=_("Distribution"),
+ required=True,
+ readonly=True
+ ),
+ as_of="devel"
+ )
- cve = Reference(ICve, title=_('External CVE reference corresponding'
- ' to this vulnerability, if any.'),
- required=False, readonly=True)
+ cve = exported(
+ Reference(
+ ICve,
+ title=_('External CVE reference corresponding'
+ ' to this vulnerability, if any.'),
+ required=False,
+ readonly=True
+ ),
+ as_of="devel"
+ )
- date_created = Datetime(
- title=_("The date this vulnerability was made public."),
- required=True, readonly=True)
+ date_created = exported(
+ Datetime(
+ title=_("The date this vulnerability was created."),
+ required=True,
+ readonly=True
+ ),
+ as_of="devel"
+ )
- creator = Reference(
- title=_('Person'), schema=IPerson, required=True, readonly=True)
+ creator = exported(
+ Reference(
+ title=_('Person'),
+ schema=IPerson,
+ required=True,
+ readonly=True
+ ),
+ as_of="devel"
+ )
class IVulnerabilityEditableAttributes(Interface):
@@ -138,51 +143,100 @@ class IVulnerabilityEditableAttributes(Interface):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- status = Choice(
- title=_('Result of the report'), readonly=True,
- required=True, vocabulary=VulnerabilityStatus)
+ status = exported(
+ Choice(
+ title=_('The status of the vulnerability.'),
+ readonly=True,
+ required=True,
+ vocabulary=VulnerabilityStatus
+ ),
+ as_of="devel"
+ )
- description = TextLine(
- title=_("A short description of the vulnerability."), required=False,
- readonly=False)
+ description = exported(
+ TextLine(
+ title=_("A short description of the vulnerability."),
+ required=False,
+ readonly=False
+ ),
+ as_of="devel"
+ )
- notes = TextLine(
- title=_("Free-form notes for this vulnerability."), required=False,
- readonly=False)
+ notes = exported(
+ TextLine(
+ title=_("Free-form notes for this vulnerability."),
+ required=False,
+ readonly=False
+ ),
+ as_of="devel"
+ )
- mitigation = TextLine(
- title=_("Explains why we're ignoring a vulnerability."),
- required=False, readonly=False)
+ mitigation = exported(
+ TextLine(
+ title=_("Explains why we're ignoring this vulnerability."),
+ required=False,
+ readonly=False
+ ),
+ as_of="devel"
+ )
- importance = Choice(title=_('Importance used to indicate work priority,'
- ' not severity'),
- vocabulary=BugTaskImportance, required=True,
- default=BugTaskImportance.UNDECIDED, readonly=True)
+ importance = exported(
+ Choice(
+ title=_('Indicates the work priority, not the severity'),
+ vocabulary=BugTaskImportance,
+ required=True,
+ default=BugTaskImportance.UNDECIDED,
+ readonly=True
+ ),
+ as_of="devel"
+ )
- importance_explanation = TextLine(
- title=_("Used to explain why our importance differs "
- "from somebody else's CVSS score."),
- required=False, readonly=False)
+ importance_explanation = exported(
+ TextLine(
+ title=_("Used to explain why our importance differs "
+ "from somebody else's CVSS score."),
+ required=False,
+ readonly=False
+ ),
+ as_of="devel"
+ )
- information_type = Choice(
- title=_("Information type"), vocabulary=InformationType,
- required=True, readonly=False, default=InformationType.PUBLIC,
- description=_(
- "Indicates privacy of the vulnerability."))
+ information_type = exported(
+ Choice(
+ title=_("Information type"),
+ vocabulary=InformationType,
+ required=True,
+ readonly=False,
+ default=InformationType.PUBLIC,
+ description=_(
+ "Indicates the privacy of the vulnerability."
+ )
+ ),
+ as_of="devel"
+ )
- date_made_public = Datetime(
- title=_("The date this vulnerability was made public."),
- required=False, readonly=False)
+ date_made_public = exported(
+ Datetime(
+ title=_("The date this vulnerability was made public."),
+ required=False,
+ readonly=False
+ ),
+ as_of="devel"
+ )
class IVulnerabilityEdit(Interface):
"""`IVulnerability` attributes that require launchpad.Edit."""
+# XXX lgp171188 2022-04-20 bug=760849: "beta" is a lie to get WADL
+# generation working. Individual attributes must set their version to
+# "devel".
+@exported_as_webservice_entry(as_of="beta")
class IVulnerability(IVulnerabilityView,
IVulnerabilityEditableAttributes,
IVulnerabilityEdit, IInformationType):
- """Contract describing a vulnerability."""
+ """A vulnerability."""
class IVulnerabilitySet(Interface):
diff --git a/lib/lp/bugs/interfaces/webservice.py b/lib/lp/bugs/interfaces/webservice.py
index f4e1425..82f0cdd 100644
--- a/lib/lp/bugs/interfaces/webservice.py
+++ b/lib/lp/bugs/interfaces/webservice.py
@@ -34,6 +34,7 @@ __all__ = [
'IStructuralSubscriptionTarget',
'IllegalRelatedBugTasksParams',
'IllegalTarget',
+ 'IVulnerability',
'NominationError',
'NominationSeriesObsoleteError',
'UserCannotEditBugTaskAssignee',
@@ -87,6 +88,7 @@ from lp.bugs.interfaces.structuralsubscription import (
IStructuralSubscription,
IStructuralSubscriptionTarget,
)
+from lp.bugs.interfaces.vulnerability import IVulnerability
_schema_circular_imports
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
index 77ebcb3..f10ece1 100644
--- a/lib/lp/bugs/model/vulnerability.py
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -19,6 +19,7 @@ from zope.component import getUtility
from zope.interface import implementer
from lp.app.enums import InformationType
+from lp.bugs.enums import VulnerabilityStatus
from lp.bugs.interfaces.buglink import IBugLinkTarget
from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.bugs.interfaces.vulnerability import (
@@ -27,7 +28,6 @@ from lp.bugs.interfaces.vulnerability import (
IVulnerabilityActivitySet,
IVulnerabilitySet,
VulnerabilityChange,
- VulnerabilityStatus,
)
from lp.bugs.model.bug import Bug
from lp.bugs.model.buglinktarget import BugLinkTargetMixin
@@ -140,6 +140,7 @@ class VulnerabilitySet:
importance_explanation,
date_made_public=date_made_public)
store.add(vulnerability)
+ store.flush()
return vulnerability
diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
index 28f7e46..303ea20 100644
--- a/lib/lp/registry/browser/distribution.py
+++ b/lib/lp/registry/browser/distribution.py
@@ -226,6 +226,16 @@ class DistributionNavigation(
else:
return series
+ @stepthrough('+vulnerability')
+ def traverse_vulnerability(self, id):
+ try:
+ id = int(id)
+ except ValueError:
+ # Not a number.
+ return None
+
+ return self.context.getVulnerability(id)
+
def traverse(self, name):
policy = self.context.default_traversal_policy
if policy == DistributionDefaultTraversalPolicy.SERIES:
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index 5ccc03d..afee0b5 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -59,6 +59,7 @@ from zope.schema import (
from lp import _
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.interfaces.questiontarget import IQuestionTarget
+from lp.app.enums import InformationType
from lp.app.errors import NameLookupFailed
from lp.app.interfaces.informationtype import IInformationType
from lp.app.interfaces.launchpad import (
@@ -71,6 +72,7 @@ from lp.app.interfaces.launchpad import (
from lp.app.validators.name import name_validator
from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
from lp.blueprints.interfaces.sprint import IHasSprints
+from lp.bugs.enums import VulnerabilityStatus
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import (
IBugTarget,
@@ -78,6 +80,8 @@ from lp.bugs.interfaces.bugtarget import (
IOfficialBugTagTargetPublic,
IOfficialBugTagTargetRestricted,
)
+from lp.bugs.interfaces.bugtask import BugTaskImportance
+from lp.bugs.interfaces.cve import ICve
from lp.bugs.interfaces.structuralsubscription import (
IStructuralSubscriptionTarget,
)
@@ -98,6 +102,7 @@ from lp.registry.interfaces.milestone import (
IHasMilestones,
)
from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
+from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.pillar import (
IHasSharingPolicies,
IPillar,
@@ -475,6 +480,14 @@ class IDistributionView(
has_current_commercial_subscription = Attribute(
"Whether the distribution has a current commercial subscription.")
+ vulnerabilities = exported(
+ doNotSnapshot(CollectionField(
+ description=_("Vulnerabilities in this distribution."),
+ readonly=True,
+ # Really IVulnerability, see _schema_circular_imports.py.
+ value_type=Object(schema=Interface)))
+ )
+
def getArchiveIDList(archive=None):
"""Return a list of archive IDs suitable for sqlvalues() or quote().
@@ -797,6 +810,9 @@ class IDistributionView(
"images in this distribution to a registry."),
required=False, readonly=False)
+ def getVulnerability(vulnerability_id):
+ """Return the vulnerability in this distribution with the given id."""
+
class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
"""IDistribution properties requiring launchpad.Edit permission."""
@@ -873,6 +889,70 @@ class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
def deleteOCICredentials():
"""Delete any existing OCI credentials for the distribution."""
+ @call_with(creator=REQUEST_USER)
+ @operation_parameters(
+ status=Choice(
+ title=_('The status of the vulnerability.'),
+ required=True,
+ vocabulary=VulnerabilityStatus,
+ ),
+ creator=Reference(
+ title=_('Person creating the vulnerability.'),
+ schema=IPerson,
+ required=True,
+ ),
+ importance=Choice(
+ title=_('Indicates the work priority, not the severity. '
+ 'Defaults to `Undecided`.'),
+ vocabulary=BugTaskImportance,
+ required=False,
+ default=BugTaskImportance.UNDECIDED,
+ ),
+ information_type=Choice(
+ title=_('Information Type. Defaults to `Public`.'),
+ required=False,
+ vocabulary=InformationType,
+ default=InformationType.PUBLIC,
+ ),
+ cve=Reference(
+ ICve,
+ title=_('External CVE reference corresponding to '
+ 'this vulnerability, if any.'),
+ required=False,
+ ),
+ description=TextLine(
+ title=_('A short description of the vulnerability.'),
+ required=False,
+ ),
+ notes = TextLine(
+ title=_("Free-form notes for this vulnerability."),
+ required=False,
+ readonly=False
+ ),
+ mitigation=TextLine(
+ title=_("Explains why we're ignoring this vulnerability."),
+ required=False,
+ ),
+ importance_explanation=TextLine(
+ title=_('Used to explain why our importance differs from '
+ "somebody else's CVSS score."),
+ required=False,
+ ),
+ date_made_public=Datetime(
+ title=_("The date this vulnerability was made public."),
+ required=False,
+ ),
+ )
+ @export_write_operation()
+ @operation_for_version("devel")
+ def newVulnerability(status, creator,
+ importance=BugTaskImportance.UNDECIDED,
+ information_type=InformationType.PUBLIC,
+ cve=None, description=None, notes=None,
+ mitigation=None, importance_explanation=None,
+ date_made_public=None):
+ """Create a new vulnerability in the distribution."""
+
@exported_as_webservice_entry(as_of="beta")
class IDistribution(
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 0b6dbd4..c0a3198 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -94,7 +94,9 @@ from lp.bugs.interfaces.bugtarget import (
BUG_POLICY_ALLOWED_TYPES,
BUG_POLICY_DEFAULT_TYPES,
)
+from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
+from lp.bugs.interfaces.vulnerability import IVulnerabilitySet
from lp.bugs.model.bugtarget import (
BugTargetBase,
OfficialBugTagTargetMixin,
@@ -103,6 +105,7 @@ from lp.bugs.model.bugtaskflat import BugTaskFlat
from lp.bugs.model.structuralsubscription import (
StructuralSubscriptionTargetMixin,
)
+from lp.bugs.model.vulnerability import Vulnerability
from lp.code.interfaces.seriessourcepackagebranch import (
IFindOfficialBranchLinks,
)
@@ -1838,6 +1841,27 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
self.oci_registry_credentials = None
old_credentials.destroySelf()
+ def newVulnerability(self, status, creator,
+ importance=BugTaskImportance.UNDECIDED,
+ information_type=InformationType.PUBLIC,
+ cve=None, description=None, notes=None,
+ mitigation=None, importance_explanation=None,
+ date_made_public=None):
+ """See `IDistribution`."""
+ return getUtility(IVulnerabilitySet).new(
+ self,
+ status,
+ importance,
+ creator,
+ information_type,
+ cve,
+ description,
+ notes,
+ mitigation,
+ importance_explanation,
+ date_made_public,
+ )
+
@cachedproperty
def _known_viewers(self):
"""A set of known persons able to view this distribution."""
@@ -1874,6 +1898,21 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
DistributionSet.getDistributionPrivacyFilter(user.person),
).is_empty()
+ @cachedproperty
+ def vulnerabilities(self):
+ """See `IDistribution`."""
+ return Store.of(self).find(
+ Vulnerability,
+ Vulnerability.distribution == self,
+ )
+
+ def getVulnerability(self, vulnerability_id):
+ """See `IDistribution`."""
+ return Store.of(self).find(
+ Vulnerability,
+ distribution=self,
+ id=vulnerability_id).one()
+
@implementer(IDistributionSet)
class DistributionSet:
diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
index 5270940..daffc9b 100644
--- a/lib/lp/registry/tests/test_distribution.py
+++ b/lib/lp/registry/tests/test_distribution.py
@@ -4,6 +4,7 @@
"""Tests for Distribution."""
import datetime
+import urllib.parse
from fixtures import FakeLogger
from lazr.lifecycle.snapshot import Snapshot
@@ -12,9 +13,13 @@ import soupmatchers
from storm.store import Store
from testtools import ExpectedException
from testtools.matchers import (
+ ContainsDict,
+ Equals,
MatchesAll,
MatchesAny,
+ MatchesStructure,
Not,
+ StartsWith,
)
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
@@ -35,7 +40,9 @@ from lp.app.interfaces.services import IService
from lp.blueprints.model.specification import (
SPECIFICATION_POLICY_ALLOWED_TYPES,
)
+from lp.bugs.enums import VulnerabilityStatus
from lp.bugs.interfaces.bugtarget import BUG_POLICY_ALLOWED_TYPES
+from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
from lp.oci.tests.helpers import OCIConfigHelperMixin
from lp.registry.enums import (
@@ -1813,3 +1820,285 @@ class TestDistributionWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
france, MirrorContent.RELEASE)
self.assertTrue(len(mirrors) > 1, "Not enough mirrors")
self.assertEqual(main_mirror, mirrors[-1])
+
+
+class TestDistributionVulnerabilities(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_vulnerabilities_no_vulnerability_present(self):
+ distribution = self.factory.makeDistribution()
+ self.assertEqual(0, distribution.vulnerabilities.count())
+
+ def test_vulnerabilities_vulnerabilities_present(self):
+ distribution = self.factory.makeDistribution()
+ first_vulnerability = self.factory.makeVulnerability(
+ distribution
+ )
+ second_vulnerability = self.factory.makeVulnerability(
+ distribution
+ )
+ self.assertEqual(
+ {first_vulnerability, second_vulnerability},
+ set(distribution.vulnerabilities),
+ )
+
+ def test_newVulnerability_default_arguments(self):
+ distribution = self.factory.makeDistribution()
+ owner = distribution.owner
+
+ with person_logged_in(owner):
+ # The distribution owner can create a new vulnerability in
+ # the distribution.
+ vulnerability = distribution.newVulnerability(
+ status=VulnerabilityStatus.NEEDS_TRIAGE,
+ creator=owner,
+ )
+
+ self.assertThat(
+ vulnerability,
+ MatchesStructure.byEquality(
+ status=VulnerabilityStatus.NEEDS_TRIAGE,
+ creator=owner,
+ importance=BugTaskImportance.UNDECIDED,
+ information_type=InformationType.PUBLIC,
+ cve=None,
+ description=None,
+ notes=None,
+ mitigation=None,
+ importance_explanation=None,
+ date_made_public=None
+ )
+ )
+
+ def test_newVulnerability_all_parameters(self):
+ distribution = self.factory.makeDistribution()
+ owner = distribution.owner
+ cve = self.factory.makeCVE(sequence='2022-1234')
+ now = datetime.datetime.now(pytz.UTC)
+
+ with person_logged_in(owner):
+ # The distribution owner can create a new vulnerability in
+ # the distribution.
+ vulnerability = distribution.newVulnerability(
+ status=VulnerabilityStatus.ACTIVE,
+ creator=owner,
+ importance=BugTaskImportance.CRITICAL,
+ information_type=InformationType.PRIVATESECURITY,
+ cve=cve,
+ description='Vulnerability',
+ notes='lgp171188> Foo bar',
+ mitigation='Foo bar baz',
+ importance_explanation='Foo bar baz',
+ date_made_public=now,
+ )
+ self.assertThat(
+ vulnerability,
+ MatchesStructure.byEquality(
+ status=VulnerabilityStatus.ACTIVE,
+ creator=owner,
+ importance=BugTaskImportance.CRITICAL,
+ information_type=InformationType.PRIVATESECURITY,
+ cve=cve,
+ description='Vulnerability',
+ notes='lgp171188> Foo bar',
+ mitigation='Foo bar baz',
+ importance_explanation='Foo bar baz',
+ date_made_public=now,
+ )
+ )
+
+ def test_getVulnerability_non_existent_id(self):
+ distribution = self.factory.makeDistribution()
+ vulnerability = distribution.getVulnerability(9999999)
+ self.assertIsNone(vulnerability)
+
+ def test_getVulnerability(self):
+ distribution = self.factory.makeDistribution()
+ vulnerability = self.factory.makeVulnerability(distribution)
+ self.assertEqual(
+ vulnerability,
+ distribution.getVulnerability(
+ removeSecurityProxy(vulnerability).id
+ )
+ )
+
+
+class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_vulnerability_api_url_data(self):
+ distribution = self.factory.makeDistribution()
+ person = distribution.owner
+ cve = self.factory.makeCVE('2022-1234')
+ now = datetime.datetime.now(pytz.UTC)
+ vulnerability = removeSecurityProxy(
+ self.factory.makeVulnerability(
+ distribution,
+ creator=person,
+ cve=cve,
+ description="Foo bar baz",
+ notes="Foo bar baz",
+ mitigation="Foo bar baz",
+ importance_explanation="Foo bar baz",
+ date_made_public=now
+ )
+ )
+ vulnerability_url = api_url(vulnerability)
+
+ webservice = webservice_for_person(
+ person,
+ permission=OAuthPermission.WRITE_PRIVATE,
+ default_api_version="devel"
+ )
+ with person_logged_in(person):
+ distribution_url = webservice.getAbsoluteUrl(api_url(distribution))
+ cve_url = webservice.getAbsoluteUrl(api_url(cve))
+ creator_url = webservice.getAbsoluteUrl(api_url(person))
+
+ response = webservice.get(
+ vulnerability_url
+ )
+ self.assertEqual(200, response.status)
+
+ self.assertThat(response.jsonBody(), ContainsDict({
+ "id": Equals(vulnerability.id),
+ "distribution_link": Equals(distribution_url),
+ "cve_link": Equals(cve_url),
+ "creator_link": Equals(creator_url),
+ "status": Equals("Needs triage"),
+ "description": Equals("Foo bar baz"),
+ "notes": Equals("Foo bar baz"),
+ "mitigation": Equals("Foo bar baz"),
+ "importance": Equals("Undecided"),
+ "importance_explanation": Equals("Foo bar baz"),
+ "information_type": Equals("Public"),
+ "date_made_public": StartsWith(now.strftime("%Y-%m-%dT%H:%M:%S")),
+ }))
+
+ def test_vulnerability_api_url_invalid_id(self):
+ person = self.factory.makePerson()
+ distribution = self.factory.makeDistribution(owner=person)
+ webservice = webservice_for_person(
+ person,
+ permission=OAuthPermission.WRITE_PRIVATE,
+ default_api_version="devel"
+ )
+ with person_logged_in(person):
+ distribution_url = api_url(distribution)
+ invalid_vulnerability_url = urllib.parse.urljoin(
+ distribution_url, '/+vulnerability/foo'
+ )
+ response = webservice.get(invalid_vulnerability_url)
+ self.assertEqual(404, response.status)
+
+ def test_vulnerability_api_url_nonexistent_id(self):
+ person = self.factory.makePerson()
+ distribution = self.factory.makeDistribution(owner=person)
+ webservice = webservice_for_person(
+ person,
+ permission=OAuthPermission.WRITE_PRIVATE,
+ default_api_version="devel"
+ )
+ with person_logged_in(person):
+ distribution_url = api_url(distribution)
+ vulnerability_url = urllib.parse.urljoin(
+ distribution_url, '/+vulnerability/99999999'
+ )
+ response = webservice.get(vulnerability_url)
+ self.assertEqual(404, response.status)
+
+ def test_vulnerabilities_collection_link(self):
+ person = self.factory.makePerson()
+ distribution = self.factory.makeDistribution(owner=person)
+ cve = self.factory.makeCVE('2022-1234')
+ another_cve = self.factory.makeCVE('2022-1235')
+ now = datetime.datetime.now(pytz.UTC)
+
+ first_vulnerability = removeSecurityProxy(
+ self.factory.makeVulnerability(
+ distribution,
+ creator=person,
+ cve=cve,
+ description="Foo bar baz",
+ notes="Foo bar baz",
+ mitigation="Foo bar baz",
+ importance_explanation="Foo bar baz",
+ date_made_public=now
+ )
+ )
+ second_vulnerability = removeSecurityProxy(
+ self.factory.makeVulnerability(
+ distribution,
+ creator=person,
+ cve=another_cve,
+ description="A B C",
+ notes="A B C",
+ mitigation="A B C",
+ importance_explanation="A B C",
+ date_made_public=now
+ )
+ )
+
+ distribution_url = api_url(distribution)
+ webservice = webservice_for_person(
+ person,
+ permission=OAuthPermission.WRITE_PRIVATE,
+ default_api_version="devel"
+ )
+ with person_logged_in(person):
+ distribution_url = webservice.getAbsoluteUrl(api_url(distribution))
+ cve_url = webservice.getAbsoluteUrl(api_url(cve))
+ another_cve_url = webservice.getAbsoluteUrl(api_url(another_cve))
+ creator_url = webservice.getAbsoluteUrl(api_url(person))
+
+ response = webservice.get(
+ distribution_url
+ )
+ response_json = response.jsonBody()
+ self.assertEqual(200, response.status)
+ self.assertIn('vulnerabilities_collection_link', response_json)
+
+ response = webservice.get(
+ response_json['vulnerabilities_collection_link']
+ )
+ response_json = response.jsonBody()
+ self.assertEqual(200, response.status)
+ self.assertEqual(2, len(response_json['entries']))
+ self.assertThat(
+ response_json['entries'][0],
+ ContainsDict({
+ "id": Equals(first_vulnerability.id),
+ "distribution_link": Equals(distribution_url),
+ "cve_link": Equals(cve_url),
+ "creator_link": Equals(creator_url),
+ "status": Equals("Needs triage"),
+ "description": Equals("Foo bar baz"),
+ "notes": Equals("Foo bar baz"),
+ "mitigation": Equals("Foo bar baz"),
+ "importance": Equals("Undecided"),
+ "importance_explanation": Equals("Foo bar baz"),
+ "information_type": Equals("Public"),
+ "date_made_public": StartsWith(
+ now.strftime("%Y-%m-%dT%H:%M:%S")
+ ),
+ }))
+ self.assertThat(
+ response_json['entries'][1],
+ ContainsDict({
+ "id": Equals(second_vulnerability.id),
+ "distribution_link": Equals(distribution_url),
+ "cve_link": Equals(another_cve_url),
+ "creator_link": Equals(creator_url),
+ "status": Equals("Needs triage"),
+ "description": Equals("A B C"),
+ "notes": Equals("A B C"),
+ "mitigation": Equals("A B C"),
+ "importance": Equals("Undecided"),
+ "importance_explanation": Equals("A B C"),
+ "information_type": Equals("Public"),
+ "date_made_public": StartsWith(
+ now.strftime("%Y-%m-%dT%H:%M:%S")
+ ),
+ }))
Follow ups