diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc3a3f80b..844f15968 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Split getting a single preference by name from `get_preferences` method into
`get_preference` [PR 85](https://github.com/greenbone/python-gvm/pull/85)
* Added an explicit `create_container_task` method [PR 108](https://github.com/greenbone/python-gvm/pull/108)
+* Added Gmpv8 version of create_tag with resource_filter parameter and
+ plural resource_ids parameter [PR 115](https://github.com/greenbone/python-gvm/pull/115)
+* Added Gmpv8 version of modify_tag with resource_action parameter,
+ resource_filter parameter, plural resource_ids parameter [PR 115](https://github.com/greenbone/python-gvm/pull/115)
### Changed
* Aligned ALIVE_TESTS declaration with list from GSA [PR 93](https://github.com/greenbone/python-gvm/pull/93)
diff --git a/gvm/protocols/gmpv7.py b/gvm/protocols/gmpv7.py
index d6eb7ac03..c01249654 100644
--- a/gvm/protocols/gmpv7.py
+++ b/gvm/protocols/gmpv7.py
@@ -5269,7 +5269,7 @@ def modify_tag(
name (str, optional): Name of the tag.
value (str, optional): Value of the tag.
active (boolean, optional): Whether the tag is active.
- resource_id (str, optional): IDs of the resource to which to
+ resource_id (str, optional): ID of the resource to which to
attach the tag. Required if resource_type is set.
resource_type (str, optional): Type of the resource to which to
attach the tag. Required if resource_id is set.
diff --git a/gvm/protocols/gmpv8.py b/gvm/protocols/gmpv8.py
index 1a74adc90..ce7aa10d8 100644
--- a/gvm/protocols/gmpv8.py
+++ b/gvm/protocols/gmpv8.py
@@ -392,3 +392,159 @@ def modify_credential(
_xmlkey.add_element("public", public_key)
return self._send_xml_command(cmd)
+
+ def create_tag(
+ self,
+ name,
+ resource_type,
+ *,
+ resource_filter=None,
+ resource_ids=None,
+ value=None,
+ comment=None,
+ active=None
+ ):
+ """Create a tag.
+
+ Arguments:
+ name (str): Name of the tag. A full tag name consisting of
+ namespace and predicate e.g. `foo:bar`.
+ resource_type (str): Entity type the tag is to be attached
+ to.
+ resource_filter (str, optional) Filter term to select
+ resources the tag is to be attached to. Either
+ resource_filter or resource_ids must be provided.
+ resource_ids (list, optional): IDs of the resources the
+ tag is to be attached to. Either resource_filter or
+ resource_ids must be provided.
+ value (str, optional): Value associated with the tag.
+ comment (str, optional): Comment for the tag.
+ active (boolean, optional): Whether the tag should be
+ active.
+
+ Returns:
+ The response. See :py:meth:`send_command` for details.
+ """
+ if not name:
+ raise RequiredArgument("create_tag requires name argument")
+
+ if not resource_filter and not resource_ids:
+ raise RequiredArgument(
+ "create_tag requires resource_filter or resource_ids argument"
+ )
+
+ if not resource_type:
+ raise RequiredArgument("create_tag requires resource_type argument")
+
+ cmd = XmlCommand('create_tag')
+ cmd.add_element('name', name)
+
+ _xmlresources = cmd.add_element("resources")
+ if resource_filter is not None:
+ _xmlresources.set_attribute("filter", resource_filter)
+
+ for resource_id in resource_ids or []:
+ _xmlresources.add_element(
+ "resource", attrs={"id": str(resource_id)}
+ )
+
+ _xmlresources.add_element("type", resource_type)
+
+ if comment:
+ cmd.add_element("comment", comment)
+
+ if value:
+ cmd.add_element("value", value)
+
+ if active is not None:
+ if active:
+ cmd.add_element("active", "1")
+ else:
+ cmd.add_element("active", "0")
+
+ return self._send_xml_command(cmd)
+
+ def modify_tag(
+ self,
+ tag_id,
+ *,
+ comment=None,
+ name=None,
+ value=None,
+ active=None,
+ resource_action=None,
+ resource_type=None,
+ resource_filter=None,
+ resource_ids=None
+ ):
+ """Modifies an existing tag.
+
+ Arguments:
+ tag_id (str): UUID of the tag.
+ comment (str, optional): Comment to add to the tag.
+ name (str, optional): Name of the tag.
+ value (str, optional): Value of the tag.
+ active (boolean, optional): Whether the tag is active.
+ resource_action (str, optional) Whether to add or remove
+ resources instead of overwriting. One of '', 'add',
+ 'set' or 'remove'.
+ resource_type (str, optional): Type of the resources to
+ which to attach the tag. Required if resource_filter
+ or resource_ids is set.
+ resource_filter (str, optional) Filter term to select
+ resources the tag is to be attached to. Required if
+ resource_type is set unless resource_ids is set.
+ resource_ids (list, optional): IDs of the resources to
+ which to attach the tag. Required if resource_type is
+ set unless resource_filter is set.
+
+ Returns:
+ The response. See :py:meth:`send_command` for details.
+ """
+ if not tag_id:
+ raise RequiredArgument("modify_tag requires a tag_id element")
+
+ cmd = XmlCommand("modify_tag")
+ cmd.set_attribute("tag_id", str(tag_id))
+
+ if comment:
+ cmd.add_element("comment", comment)
+
+ if name:
+ cmd.add_element("name", name)
+
+ if value:
+ cmd.add_element("value", value)
+
+ if active is not None:
+ cmd.add_element("active", _to_bool(active))
+
+ if resource_action or resource_filter or resource_ids or resource_type:
+ if not resource_filter and not resource_ids:
+ raise RequiredArgument(
+ "modify_tag requires resource_filter or resource_ids "
+ "argument when resource_action or resource_type is set"
+ )
+
+ if not resource_type:
+ raise RequiredArgument(
+ "modify_tag requires resource_type argument when "
+ "resource_action or resource_filter or resource_ids "
+ "is set"
+ )
+
+ _xmlresources = cmd.add_element("resources")
+ if resource_action is not None:
+ _xmlresources.set_attribute("action", resource_action)
+
+ if resource_filter is not None:
+ _xmlresources.set_attribute("filter", resource_filter)
+
+ for resource_id in resource_ids or []:
+ _xmlresources.add_element(
+ "resource", attrs={"id": str(resource_id)}
+ )
+
+ _xmlresources.add_element("type", resource_type)
+
+ return self._send_xml_command(cmd)
diff --git a/tests/protocols/gmpv8/test_create_tag.py b/tests/protocols/gmpv8/test_create_tag.py
new file mode 100644
index 000000000..622776542
--- /dev/null
+++ b/tests/protocols/gmpv8/test_create_tag.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2018 Greenbone Networks GmbH
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from gvm.errors import RequiredArgument
+from gvm.protocols.gmpv8 import Gmp
+
+from .. import MockConnection
+
+
+class GmpCreateTagTestCase(unittest.TestCase):
+ def setUp(self):
+ self.connection = MockConnection()
+ self.gmp = Gmp(self.connection)
+
+ def test_create_tag_missing_name(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name=None, resource_ids=['foo'], resource_type='task'
+ )
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='', resource_ids=['foo'], resource_type='task'
+ )
+
+ def test_create_tag_missing_resource_filter_and_ids(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type='task',
+ resource_filter=None,
+ resource_ids=None
+ )
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type='task',
+ resource_filter=None,
+ resource_ids=[]
+ )
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type='task'
+ )
+
+ def test_create_tag_missing_resource_type(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type=None,
+ resource_filter=None,
+ resource_ids=['foo']
+ )
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type=None,
+ resource_filter="name=foo",
+ resource_ids=None
+ )
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.create_tag(
+ name='foo',
+ resource_type='',
+ resource_ids=['foo']
+ )
+
+ def test_create_tag_with_resource_filter(self):
+ self.gmp.create_tag(
+ name='foo', resource_filter='name=foo', resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_create_tag_with_resource_ids(self):
+ self.gmp.create_tag(
+ name='foo', resource_ids=['foo'], resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ self.gmp.create_tag(
+ name='foo', resource_ids=['foo', 'bar'], resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_create_tag_with_comment(self):
+ self.gmp.create_tag(
+ name='foo',
+ resource_ids=['foo'],
+ resource_type='task',
+ comment='bar'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ 'task'
+ ''
+ 'bar'
+ ''
+ )
+
+ def test_create_tag_with_value(self):
+ self.gmp.create_tag(
+ name='foo', resource_ids=['foo'], resource_type='task', value='bar'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ 'task'
+ ''
+ 'bar'
+ ''
+ )
+
+ def test_create_tag_with_active(self):
+ self.gmp.create_tag(
+ name='foo', resource_ids=['foo'], resource_type='task', active=True
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ 'task'
+ ''
+ '1'
+ ''
+ )
+
+ self.gmp.create_tag(
+ name='foo', resource_ids=['foo'], resource_type='task', active=False
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ ''
+ 'task'
+ ''
+ '0'
+ ''
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/protocols/gmpv8/test_modify_tag.py b/tests/protocols/gmpv8/test_modify_tag.py
new file mode 100644
index 000000000..8029b4fd6
--- /dev/null
+++ b/tests/protocols/gmpv8/test_modify_tag.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2018 Greenbone Networks GmbH
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from gvm.errors import RequiredArgument
+from gvm.protocols.gmpv8 import Gmp
+
+from .. import MockConnection
+
+
+class GmpModifyTagTestCase(unittest.TestCase):
+ def setUp(self):
+ self.connection = MockConnection()
+ self.gmp = Gmp(self.connection)
+
+ def test_modify_tag(self):
+ self.gmp.modify_tag(tag_id='t1')
+
+ self.connection.send.has_been_called_with('')
+
+ def test_modify_tag_missing_tag_id(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id=None)
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id='')
+
+ def test_modify_tag_with_comment(self):
+ self.gmp.modify_tag(tag_id='t1', comment='foo')
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ )
+
+ def test_modify_tag_with_value(self):
+ self.gmp.modify_tag(tag_id='t1', value='foo')
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ )
+
+ def test_modify_tag_with_name(self):
+ self.gmp.modify_tag(tag_id='t1', name='foo')
+
+ self.connection.send.has_been_called_with(
+ ''
+ 'foo'
+ ''
+ )
+
+ def test_modify_tag_with_active(self):
+ self.gmp.modify_tag(tag_id='t1', active=True)
+
+ self.connection.send.has_been_called_with(
+ ''
+ '1'
+ ''
+ )
+
+ self.gmp.modify_tag(tag_id='t1', active=False)
+
+ self.connection.send.has_been_called_with(
+ ''
+ '0'
+ ''
+ )
+
+ def test_modify_tag_with_resource_filter_and_type(self):
+ self.gmp.modify_tag(
+ tag_id='t1', resource_filter='name=foo', resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_modify_tag_with_resource_action_filter_and_type(self):
+ self.gmp.modify_tag(
+ tag_id='t1',
+ resource_action='set',
+ resource_filter='name=foo',
+ resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_modify_tag_with_resource_ids_and_type(self):
+ self.gmp.modify_tag(
+ tag_id='t1',
+ resource_ids=['r1'],
+ resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_modify_tag_with_resource_action_ids_and_type(self):
+ self.gmp.modify_tag(
+ tag_id='t1',
+ resource_action="set",
+ resource_ids=['r1'],
+ resource_type='task'
+ )
+
+ self.connection.send.has_been_called_with(
+ ''
+ ''
+ ''
+ 'task'
+ ''
+ ''
+ )
+
+ def test_modify_tag_with_missing_resource_filter_or_ids_andtype(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id='t1', resource_action='add')
+
+ def test_modify_tag_with_missing_resource_type(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id='t1', resource_ids=['r1'])
+
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id='t1', resource_filter='name=foo')
+
+ def test_modify_tag_with_missing_resource_filter_and_ids(self):
+ with self.assertRaises(RequiredArgument):
+ self.gmp.modify_tag(tag_id='t1', resource_type='task')
+
+
+if __name__ == '__main__':
+ unittest.main()