Basic ability writing¶
To write an Ability, you need to use a very short boilerplate:
# coding: utf8
from packetweaver.core.ns import *
class Ability(AbilityBase):
_info = AbilityInfo(
name='Insert your Ability name here',
)
def main(self):
# Insert your code here
return 0
To get started, you need to:
- copy/paste that boilerplate into a Python file;
- change the name in the AbilityInfo instantiation. This must be a name unique to the package that contains that Ability;
- import this Python file from the
__init__.py
file of the package that contains your Ability and add a reference to your Ability class object to theexported_abilities
of the package that contains your Ability. This is described in details in the configuration section of this documentation; - stick some code into the
main
mathod.
To check that you got that right, you can simply have the main
method
return a constant and try and run that Ability from the PacketWeaver
interactive CLI.
For example, let’s pretend that you did not change the name of the template
Ability, then you can test it like this:
./run_pw
pw> list
pw> search insert
1 insert your Ability name here
pw> use 1
pw (Insert your Ability name here)> run
0
pw>
The above template will return 0, and this will be displayed by the CLI, upon completion of this Ability.
Tip
Once you have your Ability selected in the interactive CLI, remember that
you can type the editor
command to edit its source code. Every changes will
be reloaded automatically, so that you need to type run
again to see the
result of your update.
Warning
Whenever you copy/paste an existing Ability or the template Ability,
remember to change the name
value. Failing to do so will break
PacketWeaver import mechanism
Complete the meta-data¶
The Ability name is not the only information that you may set up.
Some information are just for display purposes, when you enter the info
command after selecting an Ability in the interactive CLI. This is the case of:
- description;
- authors;
- references;
- diffusion;
- reliability.
Two other pieces of information have special purposes.
The tags are a list of strings, some predefined and standard in PacketWeaver,
some custom. Any tag can be searched (and autocompleted) when using the
search
interactive CLI command.
The type values either AbilityType.STANDALONE
or
AbilityType.COMPONENT
. A component Ability is an Ability that can only be
used as part of nested Abilities. Conversely, A standalone Ability can be used
both as a component of nested Abilities, and can be run directly from the CLI.
The rationale is that you may not want to polute your Ability listing in the CLI
with all Abilities, including some that are relatively abstract and not meant to
be run directly.
Here is a complete example to illustrate them:
_info = AbilityInfo(
name='Ability basics',
description='Demonstrate a basic ability',
tags=['myproject', Tag.EXAMPLE],
authors=['pw-team',],
references=[['PacketWeaver online documentation', 'packetweaver.readthedocs.io'],],
diffusion='public',
reliability=Reliability.RELIABLE,
type=AbilityType.STANDALONE
)
Defined constants¶
To help building a consistent searchable database of Abilities, some constants were defined. This is the case for the tags or the reliability information.
Note
Using the built-in values is not mandatory (except for the type
): you
can replace them by any string value. Just make sure that it does not make your
Ability more difficult to find by adding tags very similar to the default ones.
Contributors are encouraged to suggest new built-in tags.
All the built-in values are defined in the packetweaver/core/models/status.py source file.
The howto
method¶
The main
method is not the only method that is common to all Abilities and
that Ability developers are meant to override. The howto
method is called by
the howto
command that users may enter after selecting an Ability. While the
exact behavior is up to Ability developers, this method is meant to display
some kind of message for the user better grasp how to use the Ability they just
selected.
Feel free to provide step-by-step descriptions, to add interactions or to provide comprehensive guidance on how to use your ability.
Adding parameters to your Ability¶
Several types of parameters may be passed to an Ability. Parameters may be set directly from the CLI, or they may be passed by another Ability in case of nested Abilities.
These parameters are strongly-typed: values are automatically checked upon assignment, with an AttributeError exception being raised if the value is inappropriate. These parameters may also contain special values, which triggers value generation at running time.
To add parameters to your Ability, you need to set a class property named
_option_list
containing, as the name implies, a list of options instances.
Here follows a example of such an option list:
_option_list = [
PathOpt('path', default='/bin/true', comment='mon exe', executable=True),
PathOpt('path', default=None, comment='path to nowhere', optional=True, executable=False),
IpOpt('mon_ip', default='RandIP', comment='mon ip'),
IpOpt(ns.OptNames.IP_DST, default='127.0.0.1', comment='IP of the target'),
MacOpt(ns.OptNames.MAC_SRC, default='RandMac', comment='Mac of the sender'),
StrOpt('data', default='useful string', comment='Some data'),
NumOpt('number', default=0, comment='A number (like port, counters...'),
ChoiceOpt('action', ['run', 'stop', 'reboot'], comment='performed on the dstIp '),
]
Parameters cover various data types such as IP and MAC addresses, strings, numbers, network cards, IP subnets, booleans, file system paths or choice options.
All parameter types may also contain “None”, which can be assigned to parameters that are optional.
Parameter constructors all receive a name as a first parameter. This name is used to set and get this option value, both from the command line and from the code.
While developing an Ability, you may obtain the current parameter value
from any method of the Ability, by accessing it as a attribute from that Ability
class instance. For instance, to access the value of a BoolOpt, representing a
boolean, called my_option_name
, you may simply write:
self.my_option_name
Note
While parameter naming is free of constraints, you might want to use
some of the built-in names, that are listed in the OptNames
class in
packetweaver/core/models/status.py. Using these names in your Ability
creates a sense of consistency that makes the user safe.
Warning
Please keep in mind that if you want to access your option value using the attribute syntax, you need to keep your names within the boundaries of the Python variable naming constraints. If you want to use hyphen, spaces or whatever other invalid characters, you will need to access the parameter value using the following syntax:
self.get_opt('my name, containing spaces and punctuation')
When reading the value of a parameter containing one of the special expressions that generate data, the latest generated value is cached, so that multiple read yield the same result:
a = self.my_option_name
b = self.my_option_name
assert(a == b)
You may force the generation of a new value by asking for a cache bypass. For
this, there is no direct read of the attribute. Instead, you need to use the
get_opt
method, inherited from AbilityBase
:
a = self.my_option_name
b = self.get_opt('my_option_name', bypass_cache=True)
assert(a != b) # Most probably different, if the RNG God is nice with us :)
Boolean parameters¶
Booleans are represented by the BoolOpt
class.
Values that can be successfully assigned to a BoolOpt
are:
True
;False
;"True"
;"False"
;None
or"None"
if theBoolOpt
is optional.
You may define a default value using the default
keyword argument, when
declaring the BoolOpt
:
BoolOpt('my_bool', default=False)
The default default value is False
.
String parameters¶
Strings are represented by the StrOpt
class.
Any string may be assigned to such a parameter, except "None"
and
RandString
. The former can be assigned when the StrOpt
is
optional. The latter is a special keyword, which indicates that when
reading the parameter value, strings of random length and content must be
generated.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandString"
.
Number parameters¶
Numbers (both integers anf floats) are represented by the NumOpt
class.
Values that can be successfully assigned to a NumOpt
are:
- any integer or float in Python int/float format
- any string that can be parsed by Python standard library into an integer or float
None
or"None"
if theNumOpt
is optional;"RandByte"
to generate a random integer between 0 and 2**8;"RandSByte"
to generate a random integer between -2**7 and 2**7 - 1;"RandShort"
to generate a random integer between 0 and 2**16;"RandSShort"
to generate a random integer between -2**15 and 2**15 - 1;"RandInt"
to generate a random integer between 0 and 2**32;"RandSInt"
to generate a random integer between -2**31 and 2**31 - 1;"RandLong"
to generate a random integer between 0 and 2**64;"RandSLong"
to generate a random integer between -2**63 and 2**63 - 1.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandByte"
.
IP address parameters¶
IP addresses are represented by the IpOpt
class. The class may store any IP
address, be it in IPv4 or IPv6.
Values that can be successfully assigned to a IpOpt
are:
- any IPv4 in quad-dotted format;
- any IPv6, compressed or uncompressed;
"RandIP4"
to generate a random IPv4 address, which might be anywhere in the address space, including private networks, class D and E, and loopback;"RandIP6"
to generate completely random IPv6 address, with no guarantee that the address will be valid"RandIP_classA"
to generate a random IP within the IPv4 class A"RandIP_classB"
to generate a random IP within the IPv4 class B"RandIP_classC"
to generate a random IP within the IPv4 class C"RandIP_classD"
to generate a random IP within the IPv4 class D"RandIP_classE"
to generate a random IP within the IPv4 class E
Note
A more complete syntax is on our TODO-list to enable you to define random ranges (e.g. 192.168.10-20.*).
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandIP4"
.
Warning
IpOpt
value validation are using either the Python
ipaddress
module or the netaddr
module. If you do not have any of
these, then the validation will not be performed and just about any value
will be tolerated.
IP subnet/prefix parameters¶
IP prefixes are represented by the PrefixOpt
class. This class may store any
IP prefix, be it in IPv4 or IPv6.
This parameter type is meant to enable you to walkthrough the prefix, by generating each and every one IP address of the specified prefix. Generation by either be ordered, from the first address to the last address of the prefix (excluding the network address and the broadcast address).
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is
"127.0.0.0/8"
.
This parameter constructor also has a ordered
keyword argument, which values
False
by default. If True
, the generation of the IP address of the
prefix will be from the first address to the last one. If False
and if
prerequisites are met, the IP address will be generated randomly inside the
prefix without ever generating the same address twice. This might come in handy
when scanning large networks, if you do not want to indirectly harass a
middlebox such as a firewall that is on path with many scanned endpoints inside
a subnet.
When all IP addresses of the specified prefix are generated, the next
cache bypass raises a StopIteration
exception.
Here follows an exemple of a PrefixOpt
instantiation:
PrefixOpt('MyPrefix', default='192.0.2.0/29', ordered=True)
And the usage of such an option could be:
try:
while True:
print(self.get_opt('MyPrefix', cache_bypass=True))
except StopIteration:
pass
Caution
This parameter does not work with /31 and /32 prefixes, and it will not work either with /127 and /128 prefixes.
Warning
This parameter is heavily based on the Python ipaddress
or
netaddr
module, so you will need them to get anywhere with this option.
Also, you might need the Python gmpy2
module to have random IP address
generation from this parameter.
MAC address parameters¶
Physical addresses (MAC addresses) are represented by the MacOpt
class.
Values that can be successfully assigned to a MacOpt
are:
- any well-formated MAC address, as a string;
"Mac00"
as a shorthand for the null MAC address;"MacFF"
as s shorthand for the broadcast MAC address;"RandMac"
to generate a random MAC address;"RandMulticastMac"
to generate a random Multicast MAC address from the IPv4 multicast associated MAC address range;None
or"None"
if theMacOpt
is optional.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandMac"
.
Choice parameters¶
Choice parameters represent alternatives from which you can select one value.
The set of available choices is up to Ability developers, who must list them at
instantiation of the ChoiceOpt
class:
ChoiceOpt('favorite_food', ['pizza', 'beer', 'greenStuff'])
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is the first choice
in the specified list.
If the choice parameter is optional, the special "None"
value may be
assigned too.
Port number parameters¶
Port numbers are represented by the PortOpt
.
Values that can be successfully assigned to a PortOpt
are:
- any port number between 0 and 65535 inclusive as a Python integer or a Python string;
"RandPort"
to generate a random port number between 1 and 65535 inclusive;"RandPrivilegedPort"
to generate a random port number between 1 and 1023 inclusive.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandPort"
.
Network card parameters¶
Network card names are represented by the NicOpt
.
Valid values for NicOpt
are all network device name on the current computer,
be it the name of a network card, a bridge, a virtual Ethernet adapter, or any
other types of network devices really.
As an exception, this parameter accepts None
and "None"
even if this
parameter is not optionnal. The rationale is that network device
names vary between platform and distros and having a non-None default value
would break on random platforms. As such, the default default value is None
and it is advised not to override it.
Warning
Validation of NicOpt values is performed using the Python
pyroute2
library. If this library is missing, any value will be
accepted.
Filesystem path parameters¶
Filesystem paths are represented by PathOpt
.
Valid values for this parameter type are all strings. The valid paths may however be constrained even further using a set of keyword arguments at instanciation time. Here follows the list of the various constraints that may be specified:
must_exist
: ifTrue
, the value must be the path of an existing file; ifFalse
, the file must not exist at the time of check.None
means “do not care”;readable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have read access on that file; ifFalse
, the user running PacketWeaver must not have read access on that file.None
means “do not care”;writable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have write access on that file; ifFalse
, the user running PacketWeaver must not have write access on that file.None
means “do not care”;executable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have execute access on that file; ifFalse
, the user running PacketWeaver must not have execute access on that file.None
means “do not care”;is_dir
: ifTrue
, the path must be one of an existing directory. IfFalse
, the path may be a directory or not.
Warning
must_exist
is subject to race conditions. This constraint is
not for security purposes.
So, basically, if you want to create a PathOpt to write a log file, you might want to be sure that you are not overwriting any existing file:
PathOpt('log_file', default='/var/log/mylog.txt', must_exist=False)
Caution
must_exist=False
is incompatible with readable
,
writable
and executable
, because the file does not exist, and does
not have any ACL (yet).
Paths can be specified in an absolute or relative manner, the latter being interpreted starting from the ability’s ability package root path.
Text output¶
To help you highlight your code output, the _view
object is available to
display colored messages and block structures.
Here are some examples you may use:
self._view.success('Display in green')
self._view.delimiter('A dashed line with title') # with a fixed len
self._view.delimiter() # a dashed line with the same length
self._view.warning('Display in yellow')
self._view.error('Display in red')
self._view.fail('Display in cyan')
self._view.progress('Display in blue')
self._view.debug('Display in purple')
self._view.success('Display in your default terminal color')