Creating Plugins and Extensions for Cerbero Suite and Cerbero Engine

Introduction

Plugins and extensions are developed for Cerbero Suite and Cerbero engine in the same way. For plugins not using UI functions, it is usually enough to import the Pro.Core module. Plugins using UI functions must import the Pro.UI module. Depending on the type of plugin, it is necessary to get acquainted with at least one of these two modules.

This is a quick overview of the type of extensions that can be created:

  • Actions - To create basic type of extension which can be executed by pressing ‘Ctrl+R’.

  • Hooks - To modify existing scan operations.

  • Logic Providers - To define custom scan operations or create standalone tools.

  • Scan Providers - To add scan support for new file formats.

  • Key Providers - To provide keys to files which require a decryption key.

  • UI Hooks - To provide various UI extensions.

  • Themes - To create custom or derived UI themes.

  • Carbon Loaders - To create file format loaders for the Carbon disassembler.

Once the plugin or extension is finished, it can be deployed using Packages.

Actions

Actions are the most basic type of extension, yet very versatile and useful. Actions in Cerbero Suite can be executed by pressing ‘Ctrl+R’, they can be dependent on the current view or file format and can perform all kinds of operations.

Actions are specified in the ‘actions.cfg’ file in the ‘config’ directory.

For example:

[JSBeautify]
category = JavaScript
label = Beautify JavaScript
file = javascript.py
context = text

The section name (‘JSBeautify’) specifies the id of the action and is also the name of the function to be called in ‘file’. The ‘file’ field supports absolute paths as well, otherwise the script is loaded from ‘plugins/python’. The ‘category’ and ‘label’ specify in which category inside the execute action dialog the action should be grouped and its description. When the ‘category’ field is omitted, it defaults to ‘Other’.

The ‘context’ field is very important as it specifies when the action should be available for use. In this specific case the action can be used in any text view. An action can also be available in more than one context.

; available both in text and hex views
context = text|hex

; available in text and hex views only when text or data is selected
context = text|hex|sel

; always available even when not in a view
context = any

What follows is how to create an action which decodes some selected text from base64 and shows the decoded bytes in a new hex view.

First it is necessary to define the action in the configuration file:

[Base64Decode]
category = Samples
label = Base64 decoder
file = samples.py
context = text|sel

And then the code in ‘samples.py’:

from Pro.UI import *

def Base64Decode():
    context = proContext()
    view = context.getCurrentView()
    if view.isValid() and view.hasSelection():
        text = view.getSelectedText()
        decview = context.createView(ProView.Type_Hex, "Base64 decoded data")
        import base64
        decview.setBytes(base64.b64decode(text.encode("utf-8")))
        context.addView(decview)
    return 0

It is also possible to limit an action to one or more specific file formats:

formats = PE|ELF

In this case the action would be available only in the context of PE and ELF executables.

Furthermore, it is possible to limit an action to specific workspaces:

workspaces = Analysis|Hex

In this case the action would be available only in the analysis and hex editing workspaces.

Hooks

Hooks are an extremely powerful type of extension. Hooks allow to customize scans and do all sorts of things.

Hooks are specified in the ‘hooks.cfg’ file in the ‘config’ directory.

A minimal hook entry:

[Test Hook]
file = test_hooks.py
scanned = scanned

The code:

def scanned(sp, ud):
    print(sp.getObjectFormat())

The ‘scanned’ function gets called after every file scan and prints out the format of the object. This function is not being called from the main thread, so it’s not possible to call UI functions.

Hooks are disabled by default and can be enabled from the ‘Hooks’ page in Cerbero Suite.

To enable a hook by default from the configuration entry:

[Test Hook]
file = test_hooks.py
scanned = scanned
enable = yes

Another supported value for ‘enable’ is ‘always’, which causes the hook to be always enabled.

It is also possible to specify the scan mode for the hook:

; not specifying a mode equals to: mode = single|batch
mode = batch

Hooks can be restricted to specific file formats too:

formats = PE|SWF

What follows is a hook extension to perform a search among the disassembled code of Java Class files and include in the report only those files which contain a particular string.

The configuration entry:

[Search Java Class]
file = test_hooks.py
scanned = searchJavaClass
mode = batch
formats = Class
enable = yes

The code:

from Pro.Core import NTTextBuffer

def searchJavaClass(sp, ud):
    cl = sp.getObject()
    out = NTTextBuffer()
    cl.Disassemble(out)
    # search string
    ret = out.buffer.find("HelloWorld") != -1
    sp.include(ret)

Although the few lines above already have a purpose, it is not optimal having to change the code in order to perform different searches. Hooks can optionally implement two more callbacks: ‘init’ and ‘end’. Both these callbacks are called from the main UI thread (so that it’s safe to call UI functions). The first one is called before any scan operation is performed, while the latter after all of them have finished.

The syntax for for these callbacks is the following:

def init():
    print("init")
    return print  # returns what the other callbacks get as their 'ud' argument

def end(ud):
    ud("end")

The ‘init’ function can optionally return the user data passed on to the other callbacks. The ‘end’ function is useful to perform clean-up operations. However, the sample above doesn’t need to clean up anything, it only needs an input box to ask the user for a string to be searched. So it only needs an ‘init’ function:

[Search Java Class]
file = test_hooks.py
init = initSearchJavaClass
scanned = searchJavaClass
mode = batch
formats = Class
enable = yes

And add the new logic to the code:

from Pro.Core import NTTextBuffer
from Pro.UI import ProInput

def initSearchJavaClass():
    return ProInput.askText("Insert string:")

def searchJavaClass(sp, ud):
    if ud == None:
        return
    cl = sp.getObject()
    out = NTTextBuffer()
    cl.Disassemble(out)
    # search string
    ret = out.buffer.find(ud) != -1
    sp.include(ret)

Hooks can also be used to customize the scan results of existing scan providers.

For example, it is possible to add a custom entry during the scan of a PE file and then provide the view to display it in the workspace.

The configuration entry:

[ExtScanDataTest_1]
label = External scan data test
file = ext_data_test.py
scanning = scanning
scandata = scandata
enable = yes

The code in ‘ext_data_test.py’ in the ‘plugins/python’ directory:

from Pro.Core import *

def scanning(sp, ud):
    e = ScanEntryData()
    e.category = SEC_Info
    e.type = CT_VersionInfo
    e.otarget = "This is a test"
    sp.addHookEntry("ExtScanDataTest_1", e)

def scandata(sp, xml, dnode, sdata):
    sdata.setViews(SCANVIEW_TEXT)
    sdata.data.setData("Hello, world!")
    return True

When scanning a file, an additional entry is shown in the report. Clicking on the entry displays the data provided by the extension.

Logic Providers

Logic providers have some similarity to hooks: a few callbacks have the same name. Their role however is different. Hooks are very powerful, but their purpose is to modify the behavior of existing operations. Logic providers, on the other hand, tell the scan engine which folders and files to scan. Alternatively, logic providers can be used to create standalone tools.

Logic providers are specified in the ‘logicp.cfg’ file in the ‘config’ directory.

What follows is an example of logic provider that scans a few directories on Windows to find executables which miss the support for Data Execution Prevention (DEP).

The configuration entry:

[MissingSecFlags]
label = Missing security flags
descr = Perform a scan inside system and application directories searching for Portable Executables which lack certain security related flags.
file = missingsecflags.py
init = init
scanning = scanning

The code in ‘missingsecflags.py’:

from Pro.Core import proCoreContext

def init():
    s = proCoreContext().getSystem()
    s.addPath("C:\\Windows")
    s.addPath("C:\\Program Files")
    s.addPath("C:\\Program Files (x86)")
    return True

def scanning(sp, ud):
    if sp.getObjectFormat() == "PE":
        obj = sp.getObject()
        # exclude .NET files
        if obj.DotNETDirectory().IsValid() == False:
            # check NX_COMPAT and DYNAMIC_BASE flags
            sp.include((obj.OptionalHeader().Num("DllCharacteristics") & 0x140) != 0x140)
            return
    sp.exclude()

The configuration entry causes an additional scan button to be visible in Cerbero Suite. The icon can be customized from the configuration by specifying an ‘icon’ field (the path is relative to the media folder).

When the user clicks the logic provider scan button, the ‘init’ function of the logic provider is called.

def init():
    from Pro.Core import proCoreContext
    s = proCoreContext().getSystem()
    s.addPath("C:\\Windows")
    # ...
    return True

The ‘init’ function calls Pro.Core.ProCoreContext.getSystem(), which returns a Pro.Core.CommonSystem base class. This class can be used to initialize the scan engine. By default the system is initialized to Pro.Core.LocalSystem. A logic provider can even create its own system class and then set it with Pro.Core.ProCoreContext.setSystem().

The function then returns True. It can also return False and abort the scan operation. Any other value is passed as user argument to other callbacks such as ‘scanning’, ‘scanned’ and ‘end’.

Note

That’s a small difference between hooks and logic providers: the ‘init’ function in hooks can’t abort a scan operation. Another difference is that while hooks don’t have mandatory callbacks, the ‘init’ function is mandatory for logic providers, since without it nothing is done. Logic providers start their own scanning operations, while hooks just attach to existing operations.

The ‘scanned’ function has the same syntax as in hooks. The only thing worth mentioning is that hooks can be selectively called based on the file format (see ‘formats’ field). This isn’t true for logic providers: their ‘scanning’/’scanned’ callbacks are called for every file.

An important aspect of logic providers is that the scan engine remembers which logic providers have been used to create a report and calls their ‘rload’ callback when loading that report. The ‘rload’ callback exists even for hooks, but for them it’s called in any case provided the hook is enabled.

Important

It’s important to remember that the identifying name for logic providers is the value contained between brackets in the configuration file. If it’s changed, it won’t be possible to identify the logic provider and an error message is printed out in the output view.

Logic providers can also be used to create standalone tools. To do so, the ‘init’ function must create a workspace (Pro.UI.ProWorkspace).

For example:

[Workspace]
descr = Workspace Test
group = Example
file = ws.py
init = init

The logic provider code:

from Pro.Core import *
from Pro.UI import *

def cb(ws, ud, code, view, data):
    if code == pvnInit:
        print("hello world!")
        ws.restoreAppearance()
    elif code == pvnClose:
        print("bye world!")
    elif code == pvnExecuteAction:
        print(data.cmd)
        if data.cmd == 0:
            view = proContext().createView(ProView.Type_Hex, "testview")
            dock = ws.createDock(view, "testdock", "Test dock")
            ws.addDock(dock)
        elif data.cmd == 1:
            ws.saveLayout("d1")
            proContext().msgBox(MBIconInfo, "Saved", "Layout saved")
    return 0

def init():
    ctx = proContext()
    ws = ctx.createWorkspace("Test")
    ws.setup(cb, None)
    ws.setTitle("Test Workspace")
    ws.addStdToolBar(ws.StdToolBar_Run)
    ws.addStdToolBar(ws.StdToolBar_Views)
    ws.addStdToolBar(ws.StdToolBar_CmdLine)
    tbar = ws.addToolBar("test_tbar", "Test ToolBar")
    menu = ws.menuBar().addMenu("&File")
    for i in range(5):
        a = ws.addAction(i, "action " + str(i), "", i)
        tbar.addAction(a)
        menu.addAction(a)
        if i == 2:
            tbar.addSeparator()
            menu.addSeparator()
    for i in range(2):
        view = ctx.createView(ProView.Type_Text, "view" + str(i))
        dock = ws.createDock(view, "d" + str(i), "Test dock " + str(i))
        ws.addDock(dock)
    dock = ws.createStdDock(ws.StdDock_Output)
    ws.addDock(dock)
    ws.show()
    return False

The important thing to note is that the ‘init’ function must return False for standalone tools.

If the standalone tool doesn’t require a report to be created, an optimization consists in specifying the following extra field in the configuration:

type = tool

Logic providers can also be invoked from the command line. In which case the ‘cmd’ field must be specified:

cmd = "pkg-create"

To provide the description for the supported arguments, the ‘argshelp’ field must be specified:

argshelp = Syntax: $cmd input.zip output.cppkg|name::The unique name of the package|author::The author of the package|publisher::The publisher of the package (optional)|version::The version of the package. E.g.: $self "1.0.1"|descr::A description of the package|sign::The key to sign the package. E.g.: $self private_key.pem

Each argument is preceded by the ‘|’ symbol. What follows is the name of the argument followed by ‘::’ and its description. The description can contain variables such as ‘$cmd’ and ‘$self’, which are expanded automatically. ‘$cmd’ is the name of the command (in this case ‘pkg-create’) and ‘$self’ is the name of the current argument being described.

To retrieve its arguments a logic provider must call Pro.Core.ProCoreContext.logicProviderArguments().

def customLogicProviderInit():
    ctx = proCoreContext()
    args = ctx.logicProviderArguments()
    if not args.isEmpty():
        # has arguments...

Additionally, logic providers can provide a list of recently opened items directly in the main window of Cerbero Suite next to their entry:

recentitems = "my_recent_files"
openrecent = "file"

The ‘recentitems’ field must match the name of the history used by the extension in methods like Pro.UI.ProInput.askTextEx(). The optional ‘openrecent’ field specifies the command switch used to open the item.

When a logic provider can be invoked from the command line and has a ‘recentitems’ field, it is assumed that it can open files from disk. If that is not the case, the logic provider can specify the ‘nofiles’ flag:

flags = nofiles

If the logic provider accepts directories as input, it can specify the ‘dirs’ flags (also available in combination with the ‘nofiles’ flag):

flags = dirs|nofiles

If a package includes a settings page (UI Hooks), it can be made available directly from the logic provider launcher by specifying the name of the page:

settings = Settings Page Name

Scan Providers

Scan providers are the type of extension to add support for new file formats for scan operations.

Scan provider entries are specified in the ‘scanp.cfg’ file in the ‘config’ directory.

What follows is an example scan provider.

First the configuration entry:

[TEST]
label = Test scan provider
ext = test1,test2
group = db
file = Test.py
allocator = allocator

The name of the section specifies the name of the format being supported (in this case ‘TEST’). The hard limit for format names is 15 characters for now, this may change in the future if more are needed. The ‘label’ field contains the description. The ‘ext’ field is optional and specifies file extensions to be associated to the format. The ‘group’ field specifies the type of file which is being supported. Available groups are: img, video, audio, doc, font, exe, manexe, arch, db, sys, cert, script, mem, as, p2p, email, fs. The ‘file’ field specifies the Python source file and the ‘allocator’ field the function which returns a new instance of the scan provider class.

The code of the allocator:

def allocator():
    return TestScanProvider()

The allocator just returns a new instance of TestScanProvider, which is a class derived from Pro.Core.ScanProvider.

class TestScanProvider(ScanProvider):

    def __init__(self):
        super(TestScanProvider, self).__init__()
        self.obj = None

Every scan provider has some mandatory methods it must override.

These are the first ones:

def _clear(self):
    self.obj = None

def _getObject(self):
    return self.obj

def _initObject(self):
    self.obj = TestObject()
    self.obj.Load(self.getStream())
    return self.SCAN_RESULT_OK

The Pro.Core.ScanProvider._clear() method gives a chance to free internal resources when they’re no longer used. In Python this is not usually important as member objects are automatically be freed when their reference count reaches zero.

The Pro.Core.ScanProvider._getObject() method must return the internal instance of the object being parsed. This must be an instance of a Pro.Core.CFFObject derived class.

The Pro.Core.ScanProvider._initObject() method creates the object instance and loads the data stream into it. The example above assumes the initialization operation being successful. If that’s not the case, it must return Pro.Core.ScanProvider.SCAN_RESULT_ERROR. This method is not called by the main thread, so that it doesn’t block the UI during longer parsing operations.

The TestObject class:

class TestObject(CFFObject):

    def __init__(self):
        super(TestObject, self).__init__()
        self.SetObjectFormatName("TEST")
        self.SetDefaultEndianness(ENDIANNESS_LITTLE)

This is a minimalistic implementation of a Pro.Core.CFFObject derived class. Calling Pro.Core.CFFObject.SetDefaultEndianness() isn’t actually necessary, as every object defaults to little endian. Pro.Core.CFFObject.SetObjectFormatName() on the other hand is very important, as it sets the internal format name of the object.

The following code implements the scanning logic for the file:

def _startScan(self):
    return self.SCAN_RESULT_OK

def _threadScan(self):
    e = ScanEntryData()
    e.category = SEC_Warn
    e.type = CT_NativeCode
    self.addEntry(e)

The code above issues a single warning concerning native code. When the Pro.Core.ScanProvider._startScan() method returns Pro.Core.ScanProvider.SCAN_RESULT_OK, Pro.Core.ScanProvider._threadScan() is called from a thread other than the UI one. The logic behind this is that Pro.Core.ScanProvider._startScan() is called from the main thread and if the scan of the file doesn’t require complex operations, like in the case above, then the method could return Pro.Core.ScanProvider.SCAN_RESULT_FINISHED and then Pro.Core.ScanProvider._threadScan() is not called at all. During a threaded scan an abort request from the user can be detected via the Pro.Core.ScanProvider.isAborted() method.

From the UI side point of view, when a scan entry is clicked in the summary view, the scan provider is supposed to return UI information.

def _scanViewData(self, xml, dnode, sdata):
    if sdata.type == CT_NativeCode:
        sdata.setViews(SCANVIEW_TEXT)
        sdata.data.setData("Hello, world!")
        return True
    return False

This code displays a text field with a predefined content when the user clicks the scan entry in the summary view.

This is fairly easy, but what happens when there are several entries of the same type and the code must differentiate between them?

That’s when the Pro.Core.ScanEntryData.data member of Pro.Core.ScanEntryData plays a role. It is a string which is included in the report XML and passed again back to Pro.Core.ScanProvider._scanViewData() as an XML node.

For instance:

e.data = "<o>1234</o>"

In the final XML report becomes:

<d>
    <o>1234</o>
</d>

The ‘dnode’ argument of Pro.Core.ScanProvider._scanViewData() points to the ‘d’ node and its first child is the ‘o’ node specified when creating the entry. The ‘xml’ argument represents an instance of the Pro.Core.NTXml class, which can be used to retrieve the children of the ‘dnode’ node.

Some of the scan entries may represent embedded files (category Pro.Core.SEC_File), in which case the Pro.Core.ScanProvider._scanViewData() method must return the data representing the file.

Apart from scan entries, the scan provider can also let the user explore the format of the file. To do that, it must return a tree representing the structure of the file:

def _getFormat(self):
    ft = FormatTree()
    ft.enableIDs(True)
    fi = ft.appendChild(None, 1)
    ft.appendChild(fi, 2)
    return ft

The Pro.Core.FormatTree.enableIDs() method must be called right after creating a new Pro.Core.FormatTree class. The code above creates a format item with id ‘1’ with a child item with id ‘2’.

The method doesn’t specify neither labels nor icons for the items. This information is retrieved for each item when required through the following method:

def _formatViewInfo(self, finfo):
    if finfo.fid == 1:
        finfo.text = "directory"
        finfo.icon = PubIcon_Dir
        return True
    elif finfo.fid == 2:
        finfo.text = "entry"
        return True
    return False

The various items are identified by their id, which was specified during the creation of the tree.

The UI data for each item is retrieved through the Pro.Core.ScanProvider._formatViewData() method:

def _formatViewData(self, sdata):
    if sdata.fid == 1:
        sdata.setViews(SCANVIEW_CUSTOM)
        sdata.data.setData("<ui><hsplitter csizes='40-*'><table id='1'/><hex id='2'/></hsplitter></ui>")
        sdata.setCallback(cb, None)
        return True
    return False

The code displays a custom view with a table and a hex view separated by a splitter.

In this case the custom view has also a callback:

def cb(cv, ud, code, view, data):
    if code == pvnInit:
        pass
    return 0

Important

It is good to remember that format item ids and ids used in custom views are used to encode bookmark jumps. So if they change, saved bookmark jumps become invalid.

Here again the whole code for a better overview:

from Pro.Core import *
from Pro.UI import pvnInit, PubIcon_Dir

class TestObject(CFFObject):

    def __init__(self):
        super(TestObject, self).__init__()
        self.SetObjectFormatName("TEST")
        self.SetDefaultEndianness(ENDIANNESS_LITTLE)

def cb(cv, ud, code, view, data):
    if code == pvnInit:
        return 1
    return 0

class TestScanProvider(ScanProvider):

    def __init__(self):
        super(TestScanProvider, self).__init__()
        self.obj = None

    def _clear(self):
        self.obj = None

    def _getObject(self):
        return self.obj

    def _initObject(self):
        self.obj = TestObject()
        self.obj.Load(self.getStream())
        return self.SCAN_RESULT_OK

    def _startScan(self):
        return self.SCAN_RESULT_OK

    def _threadScan(self):
        print("thread msg")
        e = ScanEntryData()
        e.category = SEC_Warn
        e.type = CT_NativeCode
        self.addEntry(e)

    def _scanViewData(self, xml, dnode, sdata):
        if sdata.type == CT_NativeCode:
            sdata.setViews(SCANVIEW_TEXT)
            sdata.data.setData("Hello, world!")
            return True
        return False

    def _getFormat(self):
        ft = FormatTree()
        ft.enableIDs(True)
        fi = ft.appendChild(None, 1)
        ft.appendChild(fi, 2)
        return ft

    def _formatViewInfo(self, finfo):
        if finfo.fid == 1:
            finfo.text = "directory"
            finfo.icon = PubIcon_Dir
            return True
        elif finfo.fid == 2:
            finfo.text = "entry"
            return True
        return False

    def _formatViewData(self, sdata):
        if sdata.fid == 1:
            sdata.setViews(SCANVIEW_CUSTOM)
            sdata.data.setData("<ui><hsplitter csizes='40-*'><table id='1'/><hex id='2'/></hsplitter></ui>")
            sdata.setCallback(cb, None)
            return True
        return False

def allocator():
    return TestScanProvider()

The scan engine doesn’t rely on extensions alone to identify the format of a file. For external scan providers a signature mechanism based on YARA exists.

In the ‘config’ directory a ‘yara.plain’ file can be created with custom identification rules.

For example:

rule test
{
    strings:
        $sig = "test"

    condition:
        $sig at 0
}

This rule identifies the format as ‘test’ if the first 4 bytes of the file match the string ‘test’: the name of the rule identifies the format.

The file ‘yara.plain’ is compiled to the binary ‘yara.rules._[app_version]’ file at the first run. In order to refresh ‘yara.rules._[app_version]’, it must be deleted.

Important

One important thing to consider is that YARA rules aren’t matched against an entire file, but only against the first 512 bytes!

Key Providers

Key providers are a convenient way to provide keys to files which require a decryption key (e.g.: an encrypted PDF). They are powerful and easy-to-use extensions which allow to test out key dictionaries on various file formats and to avoid the all too frequent hassle of having to type common passwords.

In the case of an encrypted Zip file, the user would be prompted to enter a decryption key. While the key dialog in Cerbero Suite already has the ability to accept multiple keys and also remember them, there are things it can’t do. For example, it is not suitable for trying out key dictionaries or to generate a key based on environmental artefacts (e.g.: the name of the file requiring the decryption).

The ‘config’ directory contains a ‘keyp.cfg_sample’ file. This file can be used as template to create a key provider. As all configuration in Cerbero Suite and Cerbero Engine files, this too is an INI file.

This is what an entry for a key provider can look like:

[KeyProvider Test]
file = key_provider.py
callback = keyProvider
; this is an optional field, you can omit it and the key provider is used for any format
formats = Zip|PDF

The entry specifies where our callback is located (the relative path defaults to the ‘plugins/python’ directory) and it can also optionally specify the formats which are to be used in conjunction with this provider.

The Python code can be as simple as:

from Pro.Core import *

def keyProvider(obj, index):
    if index != 0:
        return None
    l = NTVariantList()
    l.append("password")
    return l

The provider returns a single key (‘password’). This means that when one of the specified file formats is encrypted, all registered key providers are asked to provide decryption keys. If one key works, the file is automatically decrypted.

The returned list can contain even thousands of keys, it is up to the developer to decide the amount returned. The index argument can be used to decide which bulk of keys must be returned, it starts at 0 and is incremented by l.size(). The key provider is called until a match is found or it doesn’t return any more keys.

Warning

Be careful not to return a key without checking the index, otherwise it’ll result in an endless loop.

When a string is appended to the list, it is converted internally by the conversion handlers to bytes. This means that a single string could, for instance, first be converted to utf-8, then to ascii in order to obtain a match. A key provider can also return the exact bytes to be matched. In that case, a bytes/bytearray object must be appended to the list.

The same sample could be transformed into a key generation based on environmental artefacts:

from Pro.Core import *

def keyProvider(obj, index):
    if index != 0:
        return None
    name = obj.GetStream().name()
    # do some operations involving the file name
    variable_part = ...
    l = NTVariantList()
    l.append("static_part" + variable_part)
    return l

This is useful to avoid typing passwords for archives that have a fixed decryption key schema.

UI Hooks

UI hooks can be used to provide various UI extensions. All UI hooks are specified in the ‘uihooks.cfg’ file in the ‘config’ directory.

For example, the following entry adds a page to the ‘Settings’ page in Cerbero Suite:

[SystemSettings]
label = System
category = settings
platform = windows|macos
file = uihooks.system_settings.py
init = initSystemSettings

The ‘init’ key specified the name of the function used to initialize the UI code:

def initSystemSettings(view):
    ui = [UI XML]
    view.setup(ui, viewCallback, None)
    return True

Themes

Cerbero Suite and Cerbero Engine use Qt as UI library. Qt can be skinned via CSS style-sheets. However, our products have not only many custom controls, but their own UI SDK which isn’t bound to Qt. Therefore, a theme needs to take into consideration those colors as well.

A snippet of the Monokai theme:

<theme>
    <entry name="style" value="fusion"/>

    <entry name="stylesheet">

*, QTabBar::tab {
    background-color: rgb(40, 41, 35);
    color: rgb(248, 248, 242);
    selection-background-color: rgb(72, 71, 61);
    alternate-background-color: rgb(45, 46, 40);
}

/* tab bar */

QTabBar::tab {
    padding: 6px;
}

/* ... */

    </entry>

    <!-- ... -->

    <entry name="hexed_bg_color" value="rgb(40, 41, 35)"/>
    <entry name="hexed_ro_bg_color" value="rgb(40, 41, 35)"/>
    <entry name="hexed_pen_color" value="rgb(103, 216, 239)"/>
    <entry name="hexed_sel_color" value="rgb(72, 71, 61)"/>
    <entry name="hexed_misc1_color" value="rgb(248, 248, 242)"/>
    <entry name="hexed_misc2_color" value="rgb(231, 219, 115)"/>

    <!-- ... -->
</theme>

The ‘style’ entry determines the Qt style to use for this theme. If not specified, the default style is used. The ‘stylesheet’ entry contains the CSS. The other entries represent custom colors.

Since Carbon comes with its own set of themes, the ‘themes’ directory contains a sub-directory named ‘carbon’ which contains them.

To create a new theme or to customize an existing one, a new theme file must be created in the user ‘theme’ directory.

To avoid creating an entirely new theme, it is possible to inherit from an existing one:

<theme inherits="Monokai">
    <entry name="stylesheet">

QTabBar::tab {
    padding: 16px;
}
    </entry>
</theme>

While all specified entries are simply replaced existing entries, the ‘stylesheet’ one is special, because it is appended to the inherited style-sheet. In this case the new theme does nothing else than to increase the padding of tab bar buttons. In the same way individual colors can be customized.

The inheritance of themes is supported up to a depth of five level of inheritance. Thus, it is possible to inherit from a theme which already inherits from another theme.

Carbon Loaders

Contrary to other extensions, Carbon loaders require the Pro.Carbon module. Loaders are specified in the ‘caloaders.cfg’ file in the ‘config’ directory.

The following is the entry for the ‘Raw’ format:

[Raw]
file = Pro.CarbonLdrs.Raw.py
loader = newRawLoader

The ‘loader’ key specified the function which creates the loader class.

The complete raw format loader is just a few lines of code:

from Pro.Carbon import *

class RawLoader(CarbonLoader):

    def __init__(self):
        super(RawLoader, self).__init__()

    def load(self):
        # get parameters
        p = self.carbon().getParameters()
        try:
            arch = int(p.value("arch", str(CarbonType_I_x86)), 16)
        except:
            print("carbon error: invalid arch")
            arch = CarbonType_I_x86
        try:
            base = int(p.value("base", "0"), 16)
        except:
            print("carbon error: invalid base address")
            base = 0
        # load
        db = self.carbon().getDB()
        obj = self.carbon().getObject()
        # add region
        e = caRegion()
        e.def_type_id = arch
        e.flags = caRegion.READ | caRegion.WRITE | caRegion.EXEC
        e.start = base
        e.end = e.start + obj.GetSize()
        e.offset = 0
        db.addRegion(e)
        return CARBON_OK

def newRawLoader():
    return RawLoader()

Packages

Packages are how plugins and extensions for Cerbero Suite and Cerbero Engine are deployed.

Packages can be managed in Cerbero Suite from the command line, using the Python SDK and from the UI. On Windows they can be installed from the shell context menu as well.

From the command line packages can be managed using the following syntax:

-pkg-create : Create Package
    Syntax: -pkg-create input.zip output.cppkg
    --name : The unique name of the package
    --author : The author of the package
    --publisher : The publisher of the package (optional)
    --version : The version of the package. E.g.: --version "1.0.1"
    --descr : A description of the package
    --sign : The key to sign the package. E.g.: --sign private_key.pem

-pkg-install : Install Package
    Syntax: -pkg-install package_to_install.cppkg
    --force : Silently installs unverified packages

-pkg-uninstall : Uninstall Package
    Syntax: -pkg-uninstall "Package Name"

-pkg-verify : Verify Package
    Syntax: -pkg-verify package_to_verify.cppkg

-store : Cerbero Store
    --install : The name of the package to install
    --update : The name of the package to update
    --update-all : Updates all installed packages

Similarly packages can be installed, uninstalled and verified in Cerbero Engine using the ‘ProManage.py’ script inside the local ‘python’ directory. E.g.:

python ProManage.py -pkg-install /path/to/package.cppkg

Every operation which can be performed from the command line can also be performed programmatically using the SDK.

Packages can be signed. When a package is unsigned or the signature cannot be trusted, it is shown by the installation dialog unless the --force option is specified.

A key pair for signing and verifying packages can be generated as follows:

# create the private key
openssl genrsa -out private.pem 4096

# extract the public key
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

The public key must be added to the list of trusted signers. This can be achieved by placing the generated file with the name of the issuer in the ‘certs/pkg’ directory or by using the UI.

Since packages have their own format, they can be inspected using Cerbero Suite as any other supported file format. They can also be parsed programmatically using the Pro.Package.ProPackageObject class.

Packages must have a unique name, an author, a version number of maximum 4 numeric parts and a description. Packages are created from Zip archives and they can operate in three different ways:

  1. Relying on the automatic setup, without a setup script.

  2. Relying on a setup script.

  3. Relying on both the automatic setup and a setup script.

Out of the three ways, the first one is certainly the most intuitive: all the files in the Zip archive are installed following the same directory structure as in the archive.

This means that if the archive contains a file called:

plugins/python/CustomFolder/Code.py

It will be installed in the same directory under the user folder of Cerbero Suite or Cerbero Engine.

This is true for all files, except files in the ‘config’ directory. Those files are treated specially and their contents will be appended or removed from the configuration files of the user.

So, for instance, if the following configuration for an action must be installed:

[TestAction]
category = Test
label = Text label
file = TestCode.py
context = hex

It must only be stored in the archive under ‘config/actions.cfg’ and the automatic installation/uninstallation process takes care of the rest.

Sometimes, however, an automatic installation might not be enough to install an extension. In that case a setup script called ‘setup.py’ can be provided in the archive:

def install(sctx):
    # custom operations
    return True

def uninstall(sctx):
    # custom operations
    return True

However, installing everything manually might also not be ideal. In many cases the optimal solution would be an automatic installation with only a few custom operations:

def install(sctx):
    # custom operations
    return sctx.autoInstall()

def uninstall(sctx):
    # custom operations
    return sctx.autoUninstall()

To store files in the archive which should be ignored by the automatic setup, they must be placed under a folder called ‘setup’.

Alternatively, files can be individually installed and uninstalled relying on the automatic setup using the Pro.Package.ProPackageSetupContext.installFile() and Pro.Package.ProPackageSetupContext.uninstallFile() methods of the setup context, which is passed to the functions in the setup script.

Custom extraction operations can be performed using the Pro.Package.ProPackageSetupContext.extract() method of the setup context.

An important thing to consider is that if the package is called ‘Test Package’, it will not make any difference if files are placed in the archive at the top level or under a root directory called ‘Test Package’.

For instance:

config/actions.cfg
setup.py

And:

Test Package/config/actions.cfg
Test Package/setup.py

Is considered to be the same. This way when creating the Zip archive, it can be created directly from a directory with the same name of the package.

Having a verified signature is not only good for security purposes, but also allows the package to show a custom icon in the installation dialog. The icon must be called ‘pkgicon.png’ and regardless of its size, it will be resized to a 48x48 icon when shown to the user.

What follows is an easy-to-adapt Python script to create packages using the command line of Cerbero Suite. It uses the ‘-c’ parameter, to avoid displaying message boxes.

import os, sys, shutil, subprocess

cerbero_app = r"[CERBERO_APP_PATH]"

private_key = r"[OPTIONAL_PRIVATE_KEY_PATH]"

pkg_dir = r"C:\MyPackage\TestPackage"
pkg_out = r"C:\MyPackage\TestPackage.cppkg"

pkg_name = "Test Package"
pkg_author = "Test Author"
pkg_version = "1.0.1"
pkg_descr = "Description."

shutil.make_archive(pkg_dir, "zip", pkg_dir)

args = [cerbero_app, "-c", "-pkg-create", pkg_dir + ".zip", pkg_out, "--name", pkg_name, "--author", pkg_author, "--version", pkg_version, "--descr", pkg_descr]
if private_key:
    args.append("--sign")
    args.append(private_key)

ret = subprocess.run(args).returncode
os.remove(pkg_dir + ".zip")

print("Package successfully created!" if ret == 0 else "Couldn't create package!")
sys.exit(ret)

Note

A publisher can be specified in case it differs from the author of the package. When a publisher is specified, the name of the signer of the package must match the name of the publisher.