← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master

 

Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master.

Commit message:
Add VulnerabilitySet import export endpoints

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/491584
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master.
diff --git a/lib/lp/app/browser/launchpad.py b/lib/lp/app/browser/launchpad.py
index 748ae9f..a5558df 100644
--- a/lib/lp/app/browser/launchpad.py
+++ b/lib/lp/app/browser/launchpad.py
@@ -66,6 +66,7 @@ from lp.blueprints.interfaces.specification import ISpecificationSet
 from lp.blueprints.interfaces.sprint import ISprintSet
 from lp.bugs.interfaces.bug import IBugSet
 from lp.bugs.interfaces.malone import IMaloneApplication
+from lp.bugs.interfaces.vulnerability import IVulnerabilitySet
 from lp.buildmaster.interfaces.builder import IBuilderSet
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.charms.interfaces.charmbase import ICharmBaseSet
@@ -920,6 +921,7 @@ class LaunchpadRootNavigation(Navigation):
         "testopenid": ITestOpenIDApplication,
         "questions": IQuestionSet,
         "temporary-blobs": ITemporaryStorageManager,
+        "vulnerabilities": IVulnerabilitySet,
         "+feature-rules": IFeatureRules,
         # These three have been renamed, and no redirects done, as the old
         # urls now point to the product pages.
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index e710a4f..2e5e0cc 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -864,6 +864,10 @@
             for="lp.bugs.interfaces.cve.ICveSet"
             path_expression="string:cve"
             parent_utility="lp.bugs.interfaces.malone.IMaloneApplication"/>
+        <lp:url
+            for="lp.bugs.interfaces.vulnerability.IVulnerabilitySet"
+            path_expression="string:vulnerabilities"
+            parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"/>
         <lp:navigation
             module="lp.bugs.browser.cve"
             classes="
diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
index 90a9829..bda5c1a 100644
--- a/lib/lp/bugs/interfaces/vulnerability.py
+++ b/lib/lp/bugs/interfaces/vulnerability.py
@@ -12,17 +12,29 @@ __all__ = [
 ]
 
 from lazr.enum import DBEnumeratedType, DBItem
-from lazr.restful.declarations import exported, exported_as_webservice_entry
+from lazr.restful.declarations import (
+    REQUEST_USER,
+    call_with,
+    collection_default_content,
+    export_write_operation,
+    exported,
+    exported_as_webservice_collection,
+    exported_as_webservice_entry,
+    operation_for_version,
+    operation_parameters,
+)
 from lazr.restful.fields import CollectionField, Reference
+from lazr.restful.interface import copy_field
 from zope.interface import Interface
 from zope.schema import Bool, Choice, Datetime, Int, List, TextLine
 
 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.enums import VulnerabilityHandlerEnum, VulnerabilityStatus
 from lp.bugs.interfaces.bugtask import BugTaskImportance
 from lp.bugs.interfaces.cve import ICve
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.person import IPerson
 
@@ -329,7 +341,7 @@ class IVulnerabilityEdit(Interface):
 # 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")
+@exported_as_webservice_entry(plural_name="vulnerabilities", as_of="beta")
 class IVulnerability(
     IVulnerabilityView,
     IVulnerabilityEditableAttributes,
@@ -339,6 +351,7 @@ class IVulnerability(
     """A vulnerability."""
 
 
+@exported_as_webservice_collection(IVulnerability)
 class IVulnerabilitySet(Interface):
     """The set of all vulnerabilities."""
 
@@ -381,6 +394,94 @@ class IVulnerabilitySet(Interface):
     def findByIds(vulnerability_ids, visible_by_user=None):
         """Returns the vulnerabilities with the given IDs."""
 
+    @operation_parameters(
+        handler=Choice(
+            title=_("The handler to import or export vulnerability data"),
+            readonly=True,
+            required=True,
+            vocabulary=VulnerabilityHandlerEnum,
+        ),
+        git_repository=Reference(schema=IGitRepository),
+        git_ref=TextLine(
+            title=_("The git reference to import from."),
+            required=True,
+        ),
+        git_paths=List(
+            title=_("The list of paths to import from."),
+            value_type=TextLine(),
+            required=True,
+        ),
+        information_type=copy_field(
+            IVulnerabilityEditableAttributes["information_type"]
+        ),
+        import_since_commit_sha1=TextLine(
+            title=_(
+                "Commit sha1 from which to do the import. All files added or "
+                "modified since this commit will be imported."
+            ),
+            required=False,
+        ),
+    )
+    @call_with(requester=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def importData(
+        requester,
+        handler,
+        git_repository,
+        git_ref,
+        git_paths,
+        information_type,
+        import_since_commit_sha1,
+    ):
+        """Trigger a vulnerability data import
+
+        :param handler: The handler that will perform the import.
+        :param git_repository: The git repository to import from.
+        :param git_ref: The git reference to import from.
+        :param git_paths: The paths in the git repo to import from.
+        :param information_type: The information type of the vulnerability.
+            If the git repo is private, the information type must be PRIVATE.
+        :param import_since_commit_sha1: The commit SHA1 to import from.
+            If null, import all data.
+        """
+
+    @operation_parameters(
+        handler=Choice(
+            title=_("The handler to import or export vulnerability data"),
+            readonly=True,
+            required=True,
+            vocabulary=VulnerabilityHandlerEnum,
+        ),
+        sources=List(
+            title=_(
+                "List of objects to get data from. If null, import all data."
+            ),
+            required=False,
+        ),
+    )
+    @call_with(requester=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def exportData(
+        requester,
+        handler,
+        sources,
+    ):
+        """Trigger a vulnerability data export
+
+        :param handler: The handler that will perform the export.
+        :param sources: List of objects to get data from. If null, import all
+            data.
+        """
+
+    @collection_default_content()
+    def empty_list():
+        """Return an empty collection of vulnerabilities.
+
+        This only exists to keep lazr.restful happy.
+        """
+
 
 class IVulnerabilityActivity(Interface):
     """`IVulnerabilityActivity` attributes that require launchpad.View."""
diff --git a/lib/lp/bugs/interfaces/webservice.py b/lib/lp/bugs/interfaces/webservice.py
index 3524273..e69d8e8 100644
--- a/lib/lp/bugs/interfaces/webservice.py
+++ b/lib/lp/bugs/interfaces/webservice.py
@@ -35,6 +35,7 @@ __all__ = [
     "IllegalRelatedBugTasksParams",
     "IllegalTarget",
     "IVulnerability",
+    "IVulnerabilitySet",
     "NominationError",
     "NominationSeriesObsoleteError",
     "UserCannotEditBugTaskAssignee",
@@ -81,7 +82,7 @@ from lp.bugs.interfaces.structuralsubscription import (
     IStructuralSubscription,
     IStructuralSubscriptionTarget,
 )
-from lp.bugs.interfaces.vulnerability import IVulnerability
+from lp.bugs.interfaces.vulnerability import IVulnerability, IVulnerabilitySet
 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py
index 52d6a13..67b9597 100644
--- a/lib/lp/bugs/model/tests/test_vulnerability.py
+++ b/lib/lp/bugs/model/tests/test_vulnerability.py
@@ -6,6 +6,7 @@ from fixtures import MockPatch
 from storm.store import Store
 from testtools.matchers import MatchesStructure
 from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -15,7 +16,7 @@ from lp.app.errors import (
 )
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.services import IService
-from lp.bugs.enums import VulnerabilityStatus
+from lp.bugs.enums import VulnerabilityHandlerEnum, VulnerabilityStatus
 from lp.bugs.interfaces.buglink import IBugLinkTarget
 from lp.bugs.interfaces.bugtask import BugTaskImportance
 from lp.bugs.interfaces.vulnerability import (
@@ -23,6 +24,12 @@ from lp.bugs.interfaces.vulnerability import (
     IVulnerabilitySet,
     VulnerabilityChange,
 )
+from lp.bugs.interfaces.vulnerabilityjob import IImportVulnerabilityJobSource
+from lp.bugs.model.importvulnerabilityjob import ImportVulnerabilityJob
+from lp.bugs.model.vulnerability import (
+    VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG,
+    UnauthorizedVulnerabilityHandler,
+)
 from lp.bugs.model.vulnerabilitysubscription import VulnerabilitySubscription
 from lp.registry.enums import BugSharingPolicy, TeamMembershipPolicy
 from lp.services.webapp.authorization import check_permission
@@ -30,10 +37,14 @@ from lp.testing import (
     TestCaseWithFactory,
     admin_logged_in,
     anonymous_logged_in,
+    api_url,
+    feature_flags,
     person_logged_in,
+    set_feature_flag,
     verifyObject,
 )
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
 
 
 def grant_access_to_non_public_vulnerability(vulnerability, person):
@@ -639,3 +650,385 @@ class TestVulnerabilitySet(TestCaseWithFactory):
                 visible_by_user=person,
             ),
         )
+
+
+class TestVulnerabilitySetImportData(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super().setUp()
+        self.requester = self.factory.makePerson()
+        self.handler = VulnerabilityHandlerEnum.SOSS
+        self.git_ref = "ref/heads/main"
+        self.git_paths = ["cves"]
+        self.information_type = InformationType.PROPRIETARY
+        self.team = self.factory.makeTeam(members=[self.requester])
+
+    def test_importData(self):
+        """Test that we can create a ImportVulnerabilityJob using importData
+        method.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+
+        repo = self.factory.makeGitRepository(
+            owner=self.team, information_type=InformationType.USERDATA
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        with person_logged_in(self.requester):
+            getUtility(IVulnerabilitySet).importData(
+                self.requester,
+                self.handler,
+                repo,
+                self.git_ref,
+                self.git_paths,
+                self.information_type,
+                import_since_commit_sha1=None,
+            )
+
+            job = getUtility(IImportVulnerabilityJobSource).get(self.handler)
+            naked_job = removeSecurityProxy(job)
+            self.assertIsInstance(naked_job, ImportVulnerabilityJob)
+            self.assertEqual(naked_job.git_repository, repo.git_https_url)
+            self.assertEqual(naked_job.git_ref, self.git_ref)
+            self.assertEqual(naked_job.git_paths, self.git_paths)
+            self.assertEqual(
+                naked_job.information_type, self.information_type.value
+            )
+            self.assertEqual(naked_job.import_since_commit_sha1, None)
+
+    def test_importData_feature_flag_disabled(self):
+        """Test that if the feature flag is disabled it raises the
+        NotImplementedError exception."""
+
+        # All parameters None, feature flag is the first check
+        self.assertRaisesWithContent(
+            NotImplementedError,
+            "Vulnerability import API is currently disabled",
+            getUtility(IVulnerabilitySet).importData,
+            *(None,) * 7,
+        )
+
+    def test_importData_git_repo_unauthorized(self):
+        """Test that a user cannot create a ImportVulnerabilityJob for a git
+        repository that is not visible for that user.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+
+        repo = self.factory.makeGitRepository(
+            information_type=InformationType.PRIVATESECURITY
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        self.assertRaises(
+            Unauthorized,
+            getUtility(IVulnerabilitySet).importData,
+            self.requester,
+            self.handler,
+            repo,
+            self.git_ref,
+            self.git_paths,
+            self.information_type,
+            import_since_commit_sha1=None,
+        )
+
+    def test_importData_handler_unauthorized(self):
+        """Test that we cannot create a ImportVulnerabilityJob if the user is
+        not authorized to use the handler.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss")
+
+        repo = self.factory.makeGitRepository(
+            owner=self.team, information_type=InformationType.USERDATA
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        self.assertRaisesWithContent(
+            UnauthorizedVulnerabilityHandler,
+            f"You don't have enough rights to use {self.handler}",
+            getUtility(IVulnerabilitySet).importData,
+            self.requester,
+            self.handler,
+            repo,
+            self.git_ref,
+            self.git_paths,
+            self.information_type,
+            import_since_commit_sha1=None,
+        )
+
+    def test_importData_information_type_private(self):
+        """Test that we cannot create a ImportVulnerabilityJob when
+        information_type is public but the git repository is private.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+
+        repo = self.factory.makeGitRepository(
+            owner=self.team, information_type=InformationType.USERDATA
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        information_type = InformationType.PUBLIC
+        with person_logged_in(self.requester):
+            self.assertRaisesWithContent(
+                ValueError,
+                "information_type must be PRIVATE when importing from a "
+                "private git repository",
+                getUtility(IVulnerabilitySet).importData,
+                self.requester,
+                self.handler,
+                repo,
+                self.git_ref,
+                self.git_paths,
+                information_type,
+                import_since_commit_sha1=None,
+            )
+
+    def test_importData_wrong_git_ref(self):
+        """Test that we cannot create a ImportVulnerabilityJob when git_ref is
+        not in the git repository.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+
+        repo = self.factory.makeGitRepository(
+            owner=self.team, information_type=InformationType.USERDATA
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        with person_logged_in(self.requester):
+            self.assertRaisesWithContent(
+                ValueError,
+                "wrong-git-ref does not exist in the specified git repository",
+                getUtility(IVulnerabilitySet).importData,
+                self.requester,
+                self.handler,
+                repo,
+                "wrong-git-ref",
+                self.git_paths,
+                self.information_type,
+                import_since_commit_sha1=None,
+            )
+
+    def test_importData_wrong_import_since_commit_sha1(self):
+        """Test that we cannot create a ImportVulnerabilityJob when
+        import_since_commit_sha1 is not in git_ref.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+
+        repo = self.factory.makeGitRepository(
+            owner=self.team, information_type=InformationType.USERDATA
+        )
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        import_since_commit_sha1 = "1" * 40
+        with person_logged_in(self.requester):
+            self.assertRaisesWithContent(
+                ValueError,
+                f"{import_since_commit_sha1} does not exist in "
+                f"{self.git_ref}",
+                getUtility(IVulnerabilitySet).importData,
+                self.requester,
+                self.handler,
+                repo,
+                self.git_ref,
+                self.git_paths,
+                self.information_type,
+                import_since_commit_sha1,
+            )
+
+
+class TestVulnerabilitySetWebService(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super().setUp()
+        self.vulnerability_set = getUtility(IVulnerabilitySet)
+        self.vulnerability_set_url = api_url(self.vulnerability_set)
+        self.requester = self.factory.makePerson()
+        self.handler = VulnerabilityHandlerEnum.SOSS
+        self.factory.makeDistribution(name="soss", owner=self.requester)
+        self.team = self.factory.makeTeam(members=[self.requester])
+        self.git_paths = ["cves"]
+        self.information_type = InformationType.PROPRIETARY
+        self.git_ref = "ref/heads/main"
+
+    def test_importData_webservice_required_arguments_missing(self):
+        """Test that importData webservice requires missing arguments."""
+        webservice = webservice_for_person(
+            self.requester,
+            default_api_version="devel",
+        )
+        response = webservice.named_post(
+            self.vulnerability_set_url,
+            "importData",
+        )
+        self.assertEqual(400, response.status)
+        self.assertEqual(
+            {
+                "git_paths: Required input is missing.",
+                "git_ref: Required input is missing.",
+                "git_repository: Required input is missing.",
+                "handler: Required input is missing.",
+            },
+            set(response.body.decode().split("\n")),
+        )
+
+    def test_importData_webservice(self):
+        """Test that we can create a ImportVulnerabilityJob using the
+        webservice.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        repo = self.factory.makeGitRepository(owner=self.team)
+        repo_url = api_url(repo)
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        webservice = webservice_for_person(
+            self.requester,
+            default_api_version="devel",
+        )
+        response = webservice.named_post(
+            self.vulnerability_set_url,
+            "importData",
+            handler=self.handler.title,
+            git_repository=repo_url,
+            git_ref=self.git_ref,
+            git_paths=self.git_paths,
+            information_type=self.information_type.title,
+            import_since_commit_sha1=None,
+        )
+        self.assertEqual(200, response.status)
+
+    def test_importData_webservice_feature_flag_disabled(self):
+        """Test that we cannot create a ImportVulnerabilityJob using the
+        webservice when the vulnerability import API is disabled.
+        """
+        repo = self.factory.makeGitRepository(owner=self.team)
+        repo_url = api_url(repo)
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        webservice = webservice_for_person(
+            self.requester,
+            default_api_version="devel",
+        )
+        response = webservice.named_post(
+            self.vulnerability_set_url,
+            "importData",
+            handler=self.handler.title,
+            git_repository=repo_url,
+            git_ref=self.git_ref,
+            git_paths=self.git_paths,
+            information_type=self.information_type.title,
+            import_since_commit_sha1=None,
+        )
+        self.assertEqual(500, response.status)
+        self.assertEqual(
+            "Vulnerability import API is currently disabled",
+            response.body.decode().split("\n")[0],
+        )
+
+    def test_importData_webservice_git_repo_unauthorized(self):
+        """Test that we cannot create a ImportVulnerabilityJob using the
+        webservice when the git repository is not visible by the user.
+        """
+        self.useContext(feature_flags())
+        set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+        owner = self.factory.makePerson()
+        repo = self.factory.makeGitRepository(
+            owner=owner,
+            information_type=InformationType.PRIVATESECURITY,
+        )
+        with person_logged_in(owner):
+            repo_url = api_url(repo)
+
+        self.factory.makeGitRefs(
+            repository=repo,
+            paths=[self.git_ref],
+        )
+
+        webservice = webservice_for_person(
+            self.requester,
+            default_api_version="devel",
+        )
+        response = webservice.named_post(
+            self.vulnerability_set_url,
+            "importData",
+            handler=self.handler.title,
+            git_repository=repo_url,
+            git_ref=self.git_ref,
+            git_paths=self.git_paths,
+            information_type=self.information_type.title,
+            import_since_commit_sha1=None,
+        )
+
+        # 401 Unauthorized
+        self.assertEqual(401, response.status)
+
+    def test_exportData_webservice_required_arguments_missing(self):
+        """Test that exportData webservice requires missing arguments."""
+        webservice = webservice_for_person(
+            self.requester,
+            default_api_version="devel",
+        )
+        response = webservice.named_post(
+            self.vulnerability_set_url,
+            "exportData",
+        )
+        self.assertEqual(400, response.status)
+        self.assertEqual(
+            {
+                "handler: Required input is missing.",
+            },
+            set(response.body.decode().split("\n")),
+        )
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
index c33cc0f..4d4ba07 100644
--- a/lib/lp/bugs/model/vulnerability.py
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -39,6 +39,7 @@ from lp.bugs.interfaces.vulnerability import (
     IVulnerabilitySet,
     VulnerabilityChange,
 )
+from lp.bugs.interfaces.vulnerabilityjob import IImportVulnerabilityJobSource
 from lp.bugs.model.bug import Bug
 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
 from lp.bugs.model.vulnerabilitysubscription import VulnerabilitySubscription
@@ -56,8 +57,13 @@ from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
 from lp.services.database.stormbase import StormBase
 from lp.services.database.stormexpr import Array, ArrayAgg, ArrayIntersects
+from lp.services.features import getFeatureFlag
 from lp.services.xref.interfaces import IXRefSet
 
+VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG = (
+    "bugs.vulnerability_import_export.enabled"
+)
+
 
 @implementer(IVulnerability, IBugLinkTarget)
 class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
@@ -333,6 +339,10 @@ class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
 
 @implementer(IVulnerabilitySet)
 class VulnerabilitySet:
+    def __init__(self):
+        """See `IVulnerabilitySet`."""
+        self.title = "The Vulnerabilities collection"
+
     def new(
         self,
         distribution,
@@ -382,6 +392,96 @@ class VulnerabilitySet:
             clauses.append(get_vulnerability_privacy_filter(visible_by_user))
         return IStore(Vulnerability).find(Vulnerability, *clauses)
 
+    def importData(
+        self,
+        requester,
+        handler,
+        git_repository,
+        git_ref,
+        git_paths,
+        information_type,
+        import_since_commit_sha1,
+    ):
+        """See `IVulnerabilitySet`."""
+
+        if not getFeatureFlag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG):
+            raise NotImplementedError(
+                "Vulnerability import API is currently disabled"
+            )
+
+        # Launchpad's object permissions automatically check requester's
+        # permissions to git repo
+
+        # Check requester's permissions to handler
+        from lp.bugs.enums import VulnerabilityHandlerEnum
+        from lp.bugs.scripts.soss.sossimport import SOSSImporter
+
+        if handler == VulnerabilityHandlerEnum.SOSS:
+            importer = SOSSImporter()
+        else:
+            raise Exception("Handler not found")
+
+        if not importer.checkUserPermissions(requester):
+            raise UnauthorizedVulnerabilityHandler(
+                f"You don't have enough rights to use {handler}"
+            )
+
+        # If repository is private, information_type must be PRIVATE
+        if (
+            git_repository.private
+            and information_type in PUBLIC_INFORMATION_TYPES
+        ):
+            # TODO check what's usually the validation error codes we use
+            raise ValueError(
+                "information_type must be PRIVATE when importing from a "
+                "private git repository"
+            )
+
+        repo_ref = git_repository.getRefByPath(git_ref)
+        if repo_ref is None:
+            raise ValueError(
+                f"{git_ref} does not exist in the specified git repository"
+            )
+
+        # Check import_since_commit_sha1 exists in git_ref
+        if import_since_commit_sha1 and not git_repository.checkCommitInRef(
+            import_since_commit_sha1, git_ref
+        ):
+            raise ValueError(
+                f"{import_since_commit_sha1} does not exist in {git_ref}"
+            )
+
+        # Trigger the import job after validations pass
+        job_source = getUtility(IImportVulnerabilityJobSource)
+        job_source.create(
+            handler,
+            git_repository.git_https_url,
+            git_ref,
+            git_paths,
+            information_type.value,
+            import_since_commit_sha1,
+        )
+
+        return None
+
+    def exportData(
+        self,
+        requester,
+        handler,
+        sources,
+    ):
+        """See `IVulnerabilitySet`."""
+
+        raise NotImplementedError(
+            "Vulnerability export API is currently disabled"
+        )
+
+        return None
+
+    def empty_list(self):
+        """See `IVulnerabilitySet`."""
+        return []
+
 
 @implementer(IVulnerabilityActivity)
 class VulnerabilityActivity(StormBase):
@@ -506,3 +606,7 @@ def get_vulnerability_privacy_filter(user):
             policy_grant_query,
         )
     ]
+
+
+class UnauthorizedVulnerabilityHandler(Exception):
+    """The user is not allowed to use the VulnerabilityHandler."""
diff --git a/lib/lp/bugs/scripts/soss/sossimport.py b/lib/lp/bugs/scripts/soss/sossimport.py
index c96ec23..773caf4 100644
--- a/lib/lp/bugs/scripts/soss/sossimport.py
+++ b/lib/lp/bugs/scripts/soss/sossimport.py
@@ -30,10 +30,12 @@ from lp.bugs.scripts.soss.models import SOSSRecord
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.externalpackage import ExternalPackageType
 from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.externalpackage import ExternalPackage
 from lp.registry.model.person import Person
+from lp.registry.security import SecurityAdminDistribution
 from lp.testing import person_logged_in
 
 __all__ = [
@@ -476,3 +478,8 @@ class SOSSImporter:
         packagetype = first_item[0]
         package = first_item[1][0]
         return packagetype, package
+
+    def checkUserPermissions(self, user):
+        return SecurityAdminDistribution(self.soss).checkAuthenticated(
+            IPersonRoles(user)
+        )
diff --git a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
index 1c692c1..d577e6a 100644
--- a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
+++ b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
@@ -28,8 +28,11 @@ class TestSOSSImporter(TestCaseWithFactory):
             self.soss_record = SOSSRecord.from_yaml(file)
 
         self.cve = self.factory.makeCVE(sequence="2025-1979")
+        self.owner = self.factory.makePerson()
         self.soss = self.factory.makeDistribution(
-            name="soss", displayname="SOSS"
+            name="soss",
+            displayname="SOSS",
+            owner=self.owner,
         )
         transaction.commit()
 
@@ -438,3 +441,10 @@ class TestSOSSImporter(TestCaseWithFactory):
             self.soss_record, f"CVE-{self.cve.sequence}"
         )
         self.assertEqual(valid, False)
+
+    def test_checkUserPermissions(self):
+        soss_importer = SOSSImporter()
+
+        user = self.factory.makePerson()
+        self.assertEqual(soss_importer.checkUserPermissions(user), False)
+        self.assertEqual(soss_importer.checkUserPermissions(self.owner), True)
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index e40be5d..f8bd045 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -770,6 +770,9 @@ class IGitRepositoryView(IHasRecipes, IAccessTokenTarget):
         "yet been scanned."
     )
 
+    def checkCommitInRef(paths):
+        """Check if a commit exists in a git ref."""
+
     def updateMergeCommitIDs(paths):
         """Update commit SHA1s of merge proposals for this repository.
 
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 483f986..465b876 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1424,6 +1424,16 @@ class GitRepository(
         )
         return not jobs.is_empty()
 
+    def checkCommitInRef(self, commit, ref):
+        """See `IGitRepository`."""
+        store = Store.of(self)
+        return not store.find(
+            GitRef.commit_sha1,
+            GitRef.repository_id == self.id,
+            GitRef.commit_sha1 == commit,
+            GitRef.path == ref,
+        ).is_empty()
+
     def updateMergeCommitIDs(self, paths):
         """See `IGitRepository`."""
         store = Store.of(self)
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 4e48291..62f50be 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -2560,6 +2560,44 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             hosting_fixture.getRefs.extract_kwargs(),
         )
 
+        master_sha1 = hashlib.sha1(b"refs/heads/master").hexdigest()
+        author = self.factory.makePerson()
+        with person_logged_in(author):
+            author_email = author.preferredemail.email
+        author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
+        committer_date = datetime(2015, 1, 2, tzinfo=timezone.utc)
+        self.useFixture(
+            GitHostingFixture(
+                commits=[
+                    {
+                        "sha1": master_sha1,
+                        "message": "tip of master",
+                        "author": {
+                            "name": author.displayname,
+                            "email": author_email,
+                            "time": int(seconds_since_epoch(author_date)),
+                        },
+                        "committer": {
+                            "name": "New Person",
+                            "email": "new-person@xxxxxxxxxxx",
+                            "time": int(seconds_since_epoch(committer_date)),
+                        },
+                        "parents": [],
+                        "tree": hashlib.sha1(b"").hexdigest(),
+                    }
+                ]
+            )
+        )
+
+    def test_checkCommitInRef(self):
+        repository = self.factory.makeGitRepository()
+        paths = ["refs/heads/master"]
+        ref = self.factory.makeGitRefs(repository=repository, paths=paths)
+        result = repository.checkCommitInRef(
+            ref[0].commit_sha1, "refs/heads/master"
+        )
+        self.assertEqual(result, True)
+
     def test_fetchRefCommits(self):
         # fetchRefCommits fetches detailed tip commit metadata for the
         # requested refs.
diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
index 03692fe..2d8a2b6 100644
--- a/lib/lp/registry/security.py
+++ b/lib/lp/registry/security.py
@@ -6,6 +6,7 @@
 __all__ = [
     "EditByOwnersOrAdmins",
     "PublicOrPrivateTeamsExistence",
+    "SecurityAdminDistribution",
 ]
 
 from storm.expr import Select, Union
diff --git a/lib/lp/services/features/flags.py b/lib/lp/services/features/flags.py
index d904851..c4472d8 100644
--- a/lib/lp/services/features/flags.py
+++ b/lib/lp/services/features/flags.py
@@ -330,6 +330,15 @@ flag_info = sorted(
             "",
             "",
         ),
+        (
+            "bugs.vulnerability_import_export.enabled",
+            "boolean",
+            "If true, users with the right permissions can trigger "
+            "vulnerability data imports and exports via API",
+            "",
+            "",
+            "",
+        ),
     ]
 )
 

Follow ups