Linux.Utils.InstallDeb

Install a deb package and configure it with debconf answers. The package may either be specified by name, as an uploaded file or as a “tool”. If the package already exists, it may be optionally reconfigured with debconf answers.

There are three ways to specify a package (listed in order of preference if all are set):

  • DebFile: An uploaded deb package.

  • DebTool: A deb package provided as a tool, specified by tool name. Since this is a utility artifact meant to be called by other artifacts, the tool should be specified in the artifact calling this artifact. Alternatively, configure the tool using VQL .

  • DebName: The name of the package to install, or an absolute path to a deb file to install. Each word is considered as a package name or file name. apt-get interprets the package name, and allows you to specify a specific version, architecture, or even install and remove packages in the same go:

    • “foo”: installs foo
    • “foo bar- baz=1.0.0-1 qux:arm64”: installs foo, removes bar, installs a specific version of baz and a specific architecture of qux

name: Linux.Utils.InstallDeb
author: Andreas Misje  @misje
description: |
   Install a deb package and configure it with debconf answers. The package
   may either be specified by name, as an uploaded file or as a "tool". If the
   package already exists, it may be optionally reconfigured with debconf
   answers.

   There are three ways to specify a package (listed in order of preference if
   all are set):

     - DebFile: An uploaded deb package.

     - DebTool: A deb package provided as a tool, specified by tool name. Since
       this is a utility artifact meant to be called by other artifacts, the
       tool should be specified in the artifact calling this artifact.
       Alternatively, configure the tool using
       [VQL](https://docs.velociraptor.app/vql_reference/server/inventory_add/).

     - DebName: The name of the package to install, or an absolute path to a deb
       file to install. Each word is considered as a package name or file name.
       `apt-get` interprets the package name, and allows you to specify a
       specific version, architecture, or even install and remove packages in
       the same go:

       - "foo": installs foo
       - "foo bar- baz=1.0.0-1 qux:arm64": installs foo, removes bar, installs
         a specific version of baz and a specific architecture of qux

type: CLIENT

required_permissions:
   - EXECVE

reference:
   - https://manpages.debian.org/bookworm/debconf-doc/debconf-devel.7.en.html#Type

parameters:
   - name: DebName
     description: |
        Package to install (by name). Ignored if DebFile or DebTool is set. An
        absolute path to a deb file that already exists on the system is also
        accepted.

   - name: DebFile
     description: |
        Package to install (by file). Remember to click "Upload"! When set,
        DebName and DebTool is ignored. Use DebName with an absolute file path
        if the file already exists on the system and does not need to be
        uploaded.
     type: upload_file

   - name: DebTool
     description: |
        Package to install as a tool (tool name). The tool must be configured
        manually by using VQL or in another artifact (calling this artifact).
        Ignored if DebFile is set.

   - name: ToolSleepDuration
     description: |
        Maximum number of seconds to sleep before downloading the package. Only
        relevant if DebTool is set.
     type: int
     default: 20

   - name: UpdateSources
     description: |
        Run `apt-get update` before installing the package. This is not necessary
        if the package has no dependencies, and it should be disabled if there
        is no Internet.
     type: bool
     default: True

   - name: ForceConfNew
     type: bool
     description: |
        Use the configuration delivered by the package instead of keeping the
        local changes.

   - name: Reinstall
     type: bool
     description: |
        Reinstall the package if it is already installed. This is only useful
        if you know or suspect that the package installation is broken. This
        also reconfigures the package.

   - name: UpgradeOnly
     type: bool
     description: |
        Do not install the package; only upgrade it if it is already installed.

   - name: ReconfigureIfInstalled
     type: bool
     description: |
        If the package is already installed, run pre-seed debconf and
        `dpkg-reconfigure` instead.

   - name: DebConfValues
     type: csv
     description: |
        debconf is a system used by many packages for interactive configuration.
        When using a non-interactive frontend (like this artifact), answers may
        by provided as a "pre-seed" file. Example line:

        "wireshark-common/install-setuid,boolean,false"
     default: |
        Key,Type,Value

column_types:
  - name: Stdout
    type: nobreak

  - name: Stderr
    type: nobreak

sources:
  - precondition:
      SELECT OS From info() where OS = 'linux'

    query: |
       LET Tool = SELECT OSPath
         FROM Artifact.Generic.Utils.FetchBinary(ToolName=DebTool,
                                                 TemporaryOnly=true,
                                                 SleepDuration=ToolSleepDuration)
       LET Package <= if(
           condition=DebTool,
           then=Tool[0].OSPath,
           else=if(
             condition=DebFile,
             /* apt requires file names to end in an architecture name, so
                create a copy ending in "_amd64.deb". The architecture chosen
                here, amd64, does not need to match the architecture of neither
                the package or the system:
             */
             then=copy(dest=tempdir() + '/package_amd64.deb',
                       filename=DebFile),
                       else=DebName))

       /* The file name is lost from the uploaded file, so extract it from the
          package instead (apt has certain requirements for the file name):
       */
       LET PackageInfo = SELECT Stdout
         FROM execve(argv=['/usr/bin/dpkg-deb', '--field', Package, 'Package'])

       LET PackageName = if(condition=DebTool OR DebFile,
                            // remove "\n":
                            then=PackageInfo[0].Stdout[:-1], else=DebName)

       /* The file format is "package_name question type answer": */
       LET PreSeedLines = SELECT join(sep=' ',
           array=(PackageName, Key, Type, Value)) AS Line
         FROM DebConfValues

       LET PreSeedFile <= tempfile(data=join(sep='\n', array=PreSeedLines.Line))

       LET AptEnv = dict(
           DEBIAN_FRONTEND='noninteractive',
           DEBCONF_NOWARNINGS='yes')

       LET AptOpts <= ('-f', '-y', '-o', 'Debug::pkgProblemResolver=yes',
                       '--no-install-recommends') +
           if(condition=ForceConfNew,
              then=('-o', 'Dpkg::Options::=--force-confnew'), else=[]) +
           if(condition=Reinstall, then=('--reinstall', ), else=[]) +
           if(condition=UpgradeOnly, then=('--only-upgrade', ), else=[])

       LET PreSeed = SELECT 'Pre-seed debconf' AS Step, *
         FROM if(condition=DebConfValues, then={
             SELECT *
             FROM execve(argv=['/usr/bin/debconf-set-selections', PreSeedFile, ])
             WHERE log(message='Pre-seeding %v', dedup= -1,
                       args=PackageName,
                       level='INFO')
              AND (NOT ReturnCode OR log(level='ERROR',
                      message='%v failed: %v', args=(Step, Stderr)))
           })

       /* Install regardless of whether package is installed or not, handing all
          the (arch-specific) version comparison logic to apt:
        */
       LET Install <= SELECT * FROM chain(
           a_update={
             SELECT 'Updating index' AS Step, *
             FROM if(condition=UpdateSources, then={
                 SELECT *
                 FROM execve(argv=['/usr/bin/apt-get', '-y', 'update'])
                 WHERE log(message='Updating package index before installing',
                           level='INFO')
               })
           },
           b_debconf=PreSeed,
           c_install={
             SELECT 'Installing package' AS Step, *
             FROM execve(
               argv=('/usr/bin/apt-get', ) + AptOpts +
               if(condition=Package = DebName,
                  then=('install', ) + split(sep='''\s+''', string=Package),
                  else=('install', Package)),
               env=AptEnv)
             WHERE log(message='Installing deb package %v',
                       args=PackageName,
                       dedup= -1,
                       level='INFO')
           })
         WHERE NOT ReturnCode OR log(level='ERROR',
                                     message='%v failed: %v',
                                     args=(Step, Stderr))

       SELECT * FROM chain(
         a_install=Install,
         b_reconfigure={
           SELECT * FROM if(
             condition=ReconfigureIfInstalled AND (
                Reinstall OR Install.Stdout =~ 'Skipping|already the newest version'),
             then={
               SELECT 'Reconfiguring package' AS Step, *
               FROM execve(argv=['/usr/sbin/dpkg-reconfigure', PackageName, ],
                           env=AptEnv)
               WHERE log(message='Reconfiguring deb package %v',
                         args=PackageName,
                         dedup= -1,
                         level='INFO')
                AND (NOT ReturnCode OR log(level='ERROR',
                                       message='%v failed: %v',
                                       args=(Step, Stderr)))
             })
         })