Skip to content

Adding a Service Updater

This documentation builds on Developing an Assemblyline service in the event where you have a service that's dependent on signatures (ie. YARA/Suricata/Sigma rules) or on a collection of files (ie. CSV table, custom parsers) for analysis.

Build your updater

You will need to subclass the ServiceUpdater and implement/override functions as deemed necessary.

Refer to ServiceUpdater class for more details.

Let's say the following code is written in update_server.py in the root of your service directory:

from assemblyline.odm.models.signature import Signature
from assemblyline_v4_service.updater.updater import ServiceUpdater

class SampleUpdateServer(ServiceUpdater):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def import_update(self, files_sha256, client, source, default_classification): -> None
        # Purpose:  Used to import a set of signatures from a source into Assemblyline for signature management
        # Inputs:
        #   files_sha256:           A list of tuples containing file paths and their respective sha256
        #   client:                 An Assemblyline client used to interact with the API on behalf of a service account
        #   source:                 The name of the source
        #   default_classification: The default classification given to a signature if none is provided

        signatures = []
        for file, _ in files_sha256:
            # Iterate over all the files retrieved by source and upload them as signatures in Assemblyline
            with open(file, 'r') as fh:
                signatures.append(Signature(dict(
                    classification = default_classification,
                    data = fh.read(),
                    name = f'sample_signature{len(signatures)}',
                    source = source_name,
                    status = 'DEPLOYED',
                    type = 'sample'
                )))
        client.signatures.add_update_many(signatures)
        self.log.info(f'Successfully imported {len(signatures)} signatures')
        return

    def is_valid(self, file_path) -> bool:
        # Purpose:  Used to determine if the file associated is 'valid' to be processed as a signature
        # Inputs:
        #   file_path:  Path to a signature file from an external source
        return super().is_valid(file_path) #Returns true always

if __name__ == '__main__':
    with SampleUpdateServer(default_pattern="*.json") as server:
        server.serve_forever()

Let's say the following code is written in update_server.py in the root of your service directory:

from assemblyline.odm.models.signature import Signature
from assemblyline_v4_service.updater.updater import ServiceUpdater

class SampleUpdateServer(ServiceUpdater):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def import_update(self, files_sha256, client, source, default_classification): -> None
        # Purpose:  Used to import a set of signatures from a source into a reserved directory
        # Inputs:
        #   files_sha256:           A list of tuples containing file paths and their respective sha256
        #   client:                 An Assemblyline client used to interact with the API on behalf of a service account
        #   source:                 The name of the source
        #   default_classification: The default classification given to a signature if none is provided

        # You'll want to write your files to self.latest_updates_dir which should hold all your downloaded files.
        # The contents in this directory will then be used by prepare_output_directory().

        # Organize files by source
        dest_dir = os.path.join(self.latest_updates_dir, source)
        os.makedirs(dest_dir, dirs_exist_ok=True)

        # For every file in this source, move to latest updates directory in a subdirectory labelled by source.
        for file, _ in files_sha256:
          dest_file = os.path.join(dest_dir, os.path.basename(file))
          shutil.move(file, dest_file)
        self.log.info(f"Finished moving {len(files_sha256)} files for {source} to {self.latest_updates_dir}")
        return

    def is_valid(self, file_path) -> bool:
        # Purpose:  Used to determine if the file associated is 'valid' to be processed as a signature
        # Inputs:
        #   file_path:  Path to a signature file from an external source
        return super().is_valid(file_path) #Returns true always

    def prepare_output_directory(self) -> str:
        # Purpose: Prepare your downloaded sources before it's made available to your service.
        # This could be a function where if you need to compile a bunch of files together or to re-organize
        # in a certain directory format, you would call this and keep the original dataset intact.
        # Return:
        #    output_directory: the path of the directory to serve
        output_directory = tempfile.mkdtemp()
        shutil.copytree(self.latest_updates_dir, output_directory, dirs_exist_ok=True)
        return output_directory

if __name__ == '__main__':
    with SampleUpdateServer(default_pattern="*.json") as server:
        server.serve_forever()

Add it to the manifest!

In addition to your service manifest, you would append the following:

Warning

The updater dependency needs to be named updates for the service to recognize it as an updater rather than a normal dependency container.

Critical

Updaters need to run_as_core which allows them to run at the same level as other core containers.

This configuration is intended if your update files are signatures to be imported into Assemblying using the Signatures API.

# Adding your dependency called 'updates' and specify the command the container should run
dependencies:
  updates:
    container:
      allow_internet_access: true
      command: ["python", "-m", "update_server"]
      image: ${REGISTRY}testing/assemblyline-service-sample:latest
      ports: ["5003"]
    run_as_core: True

# Update configuration block
update_config:
  # list of source object from where to fetch files for update and what will be the name of those files on disk
  sources:
    - uri: https://file-examples-com.github.io/uploads/2017/02/file_example_JSON_1kb.json
      name: sample_1kb_file
  # interval in seconds at which the updater dependency runs
  update_interval_seconds: 300
  # Should the downloaded files be used to create signatures in the system
  generates_signatures: true

You'll have to indicate that this updater doesn't generate signatures (generate_signatures: false) for Assemblyline and therefore needs to be persisted to disk rather than using a client to interact with the Signatures API

This configuration sets up downloaded files to only persist on temporary storage for as long as the container runs.

# Adding your dependency called 'updates' and specify the command the container should run
dependencies:
  updates:
    container:
      allow_internet_access: true
      command: ["python", "-m", "update_server"]
      image: ${REGISTRY}testing/assemblyline-service-sample:latest
      ports: ["5003"]
    run_as_core: True

# Update configuration block
update_config:
  # list of source object from where to fetch files for update and what will be the name of those files on disk
  sources:
    - uri: https://file-examples-com.github.io/uploads/2017/02/file_example_JSON_1kb.json
      name: sample_1kb_file
  # interval in seconds at which the updater dependency runs
  update_interval_seconds: 300
  # Should the downloaded files be used to create signatures in the system
  generates_signatures: false

This configuration sets up downloaded files to only persist on a persistent volume.

Note

This will involve using the UPDATER_DIR env to tell the updater that your updates will be stored in a specific location.

# Adding your dependency called 'updates' and specify the command the container should run
dependencies:
  updates:
    container:
      allow_internet_access: true
      command: ["python", "-m", "update_server"]
      image: ${REGISTRY}testing/assemblyline-service-sample:latest
      ports: ["5003"]
      # Overwrite the UPDATER_DIR variables to point to your persisted mountpoint
      environment:
        - name: UPDATER_DIR
          value: /mnt/persistent_updates/
    # Allocate a storage volume and mount it to your updater's filesystem
    volumes:
      updates:
        mount_path: /mnt/persistent_updates/
        capacity: 5242880
        storage_class: mystorageclass
    run_as_core: True

# Update configuration block
update_config:
  # list of source object from where to fetch files for update and what will be the name of those files on disk
  sources:
    - uri: https://file-examples-com.github.io/uploads/2017/02/file_example_JSON_1kb.json
      name: sample_1kb_file
  # interval in seconds at which the updater dependency runs
  update_interval_seconds: 300
  # Should the downloaded files be used to create signatures in the system
  generates_signatures: false