Skip to content
Please update to the latest release 0.77.2 to address Multiple CVEs.
Server.Monitor.StoreClientInfo

Server.Monitor.StoreClientInfo

Save data from client interrogation as client metadata.

This artifact listens for completions of an interrogation artifact — typically Custom.Generic.Client.Info, your own override of Generic.Client.Info — and stores selected fields from the result as client metadata, so they can be indexed and searched.

Field selection is controlled by InfoMetadata, a CSV parameter mapping Source (the artifact source name, e.g. BasicInformation) and Field (the column to read) to an Alias used as the metadata key. If Alias is empty, the original Field is used. Example:

Source Field Alias
BasicInformation OS os
BasicInformation Architecture arch
ADStatus DomainJoined ad_joined
MDATPHealth MDATPHealth.edrMachineId defender_id

Field can refer to a nested field, including arrays. This means that field names containing a period (".") are not supported.

Note

The sources referred to in InfoMetadata should only return a single row. If more rows are returned, only the first is used.

See also

#metadata #automation


name: Server.Monitor.StoreClientInfo
author: Andreas Misje – @misje
description: |
  Save data from client interrogation as client metadata.

  This artifact listens for completions of an interrogation artifact —
  typically `Custom.Generic.Client.Info`, your own override of
  [`Generic.Client.Info`](/artifact_references/pages/generic.client.info/)
  — and stores selected fields from the result as client metadata, so
  they can be indexed and searched.

  Field selection is controlled by `InfoMetadata`, a CSV parameter
  mapping `Source` (the artifact source name, e.g. `BasicInformation`)
  and `Field` (the column to read) to an `Alias` used as the metadata
  key. If `Alias` is empty, the original `Field` is used. Example:

  | Source | Field | Alias |
  | ------ | ----- | ----- |
  | `BasicInformation` | `OS`  | `os` |
  | `BasicInformation` | `Architecture` | `arch` |
  | `ADStatus` | `DomainJoined` | `ad_joined` |
  | `MDATPHealth` | `MDATPHealth.edrMachineId` | `defender_id` |

  `Field` can refer to a nested field, including arrays. This means
  that field names containing a period (".") are not supported.

  > [!NOTE]
  > The sources referred to in `InfoMetadata` should only return a
  > single row. If more rows are returned, only the first is used.

  ## See also

  - [`Server.Monitor.StoreClientHWInfo`](/exchange/artifacts/pages/server.monitor.storeclienthwinfo/):
    sibling artifact tailored for hardware identification (extracting
    serial number as primary purpose)
  - [How can I automatically add & update client metadata?](/knowledge_base/tips/automating_metadata/):
    in-depth walkthrough about how to save interrogation data as
    client metadata

  #metadata #automation

type: SERVER_EVENT

parameters:
  - name: InterrogationArtifact
    type: regex
    description: |
      Name of the client artifact to watch
    default: Custom.Generic.Client.Info

  - name: InfoMetadata
    description: |
      The artifact source (e.g. BasicInformation), the field to save, and an
      optional alias to give the field as a metadata name
    type: csv
    default: |
      Source,Field,Alias
      BasicInformation,OS,os
      BasicInformation,Architecture,arch

  - name: KeepEmptyValues
    description: |
      If true, an empty value will be stored as metadata. If false, the metadata
      will not be set at all. Note that if a metadata value was previously empty,
      this will not remove that value.
    type: bool
    default: false

sources:
  - query: |
      LET ExtractSource(Artifact) = regex_replace(
          re='[^/]+(?:/(?P<Source>.+))?',
          replace='$1',
          source=Artifact)

      LET Interrogation = SELECT *
        FROM foreach(row={
          SELECT *
          FROM watch_monitoring(artifact='System.Flow.Completion')
          WHERE Flow.artifacts_with_results =~ InterrogationArtifact
        },
                     query={
          SELECT *
          FROM foreach(row=Flow.artifacts_with_results,
                       query={
          SELECT ExtractSource(Artifact=_value) AS _Source,
                 ClientId,
                 *
          FROM source(client_id=ClientId,
                      flow_id=Flow.session_id,
                      artifact=InterrogationArtifact,
                      source=ExtractSource(Artifact=_value))
          WHERE ExtractSource(Artifact=_value) IN InfoMetadata.Source
          GROUP BY _Source
        })
        })

      // With info as dict:
      LET InfoDict = SELECT _value.ClientId AS ClientId,
                            _value._Source AS _Source,
                            _value AS Data
        FROM items(item={ SELECT * FROM Interrogation })

      LET SelectedMetadata = SELECT *
        FROM foreach(row=InfoDict,
                     query={
          SELECT ClientId,
                 to_dict(item={
          SELECT *
          FROM foreach(row=InfoMetadata,
                       query={
          SELECT Alias || Field AS _key,
                 get(item=Data, member=Field) AS _value
          FROM scope()
          WHERE _Source = Source
           AND (KeepEmptyValues OR (_value != NULL
                    AND len(list=str(str=_value))))
        })
        }) - dict(ClientId=NULL, _Source=NULL) AS Metadata
          FROM scope()
        })

      // Set the metadata and return the dict of data, as well as the the
      // client_set_metadata() result:
      // Do not store an empty dict. This has proved to cause issues:
      LET SetMetadata = SELECT *, if(condition=Metadata,
                                     then=client_set_metadata(
                                       client_id=ClientId,
                                       metadata=Metadata),
                                     else=false) AS Updated
        FROM SelectedMetadata

      SELECT *
      FROM SetMetadata````