IRIS.Sync.Asset

Synchronizes client information from Velociraptor to DFIR-IRIS .

Parses available information from clients such as network interfaces, IP addresses, asset type and applied labels. Once it has been added, the asset ID from DFIR-IRIS will be added as client metadata and IRIS will be added as label.

If this artifact is applied on a client that has the asset ID set in its metadata, it won’t be readded but rather updated: Labels and the compromised status will by synchronized.

Tested with Dfir-Iris API v2.0.4 (IRIS v2.4.7)

Hints:

  • If it fails to add the client to IRIS, it will assign the IRIS-ERROR label to it. A successful run afterwards will remove it.
  • It is recommended to add the parameters with ‘Iris’ prefix to the Server Metadata to ease the usage of the artifact. The metadata can alternatively be set from a notebook using VQL similar to this example:
SELECT server_set_metadata(IrisURL="https://dfir-iris.local:4433",
                           IrisKey="This-is-an_API_KEY",
                           IrisCaseId="1",
                           IrisRootCA='''-----BEGIN CERTIFICATE-----
                           <...>
                           -----END CERTIFICATE-----'''),server_metadata() FROM scope()
  • You can define the compromise status of a system when creating and when updating the information. However, if an asset is categorized as compromised, you cannot change the status using this artifact. This is a safety measure to mitigate a potential high impact error. Beside that, you can freely change the status between No, Unknown and To be determined.
  • The true power of this artifact lies in the ability to quickly add many clients to DFIR-IRIS. As it is usually not needed to add all clients that are enrolled in Velociraptor to IRIS but rather an excerpt of important, suspicious, or compromised systems, you will most likely use this artifact from within a notebook.

Example:

to add just a few systems and have the results of the operation as JSON:

SELECT client_id,{SELECT * FROM Artifact.Exchange.IRIS.Sync.Asset(clientId=client_id,isCompromised="Y")} FROM clients(search="label:compromised")

Example to add many systems in a performant way and have the results in well-structured columns.

SELECT * FROM foreach(row={SELECT * FROM clients(search="label:suspicious")},query={SELECT * FROM Artifact.Exchange.IRIS.Sync.Asset(clientId=client_id,isCompromised="UNK")},async=true)

ATTENTION: ALWAYS USE ASYNC=FALSE IF CLIENTS ARE PRESENT IN THE TABLE MULTIPLE TIMES! OTHERWISE THESE ASSETS MIGHT BE DUPLICATED IN IRIS!!!


name: IRIS.Sync.Asset

author: Stephan Mikiss @stephmikiss (SEC Defence @SEC Consult) | Updated 2024-08 - [10root Cyber Security] (https://10root.com)

description: |
   Synchronizes client information from Velociraptor to [DFIR-IRIS](https://dfir-iris.org/).

   Parses available information from clients such as network interfaces, IP addresses, asset type and applied labels.
   Once it has been added, the asset ID from DFIR-IRIS will be added as client metadata and `IRIS` will be added as label.

   If this artifact is applied on a client that has the asset ID set in its metadata, it won't be readded but rather
   updated: Labels and the compromised status will by synchronized.

   *Tested with Dfir-Iris API v2.0.4 (IRIS v2.4.7)*

   #### Hints:

   - If it fails to add the client to IRIS, it will assign the `IRIS-ERROR` label to it. A successful run afterwards will remove it.
   - It is **recommended** to add the parameters with 'Iris' prefix to the <a href="#/host/server">Server Metadata</a> to ease the usage of the artifact. The metadata can alternatively be set from a notebook using VQL similar to this example:

   ```
   SELECT server_set_metadata(IrisURL="https://dfir-iris.local:4433",
                              IrisKey="This-is-an_API_KEY",
                              IrisCaseId="1",
                              IrisRootCA='''-----BEGIN CERTIFICATE-----
                              <...>
                              -----END CERTIFICATE-----'''),server_metadata() FROM scope()
   ```

   - You can define the compromise status of a system when creating and when updating the information. **However, if an asset is categorized as *compromised*, you cannot change the status using this artifact.** This is a safety measure to mitigate a potential high impact error. Beside that, you can freely change the status between *No*, *Unknown* and *To be determined*.
   - The true power of this artifact lies in the ability to quickly add many clients to DFIR-IRIS. As it is usually not needed to add all clients that are enrolled in Velociraptor to IRIS but rather an excerpt of important, suspicious, or compromised systems, you will *most likely use this artifact from within a notebook*.

   #### Example:

   to add just a few systems and have the results of the operation as JSON:

   ```VQL
   SELECT client_id,{SELECT * FROM Artifact.Exchange.IRIS.Sync.Asset(clientId=client_id,isCompromised="Y")} FROM clients(search="label:compromised")
   ```

   **Example** to add many systems in a performant way and have the results in well-structured columns.

   ```VQL
   SELECT * FROM foreach(row={SELECT * FROM clients(search="label:suspicious")},query={SELECT * FROM Artifact.Exchange.IRIS.Sync.Asset(clientId=client_id,isCompromised="UNK")},async=true)
   ```
   **ATTENTION: ALWAYS USE ASYNC=FALSE IF CLIENTS ARE PRESENT IN THE TABLE MULTIPLE TIMES! OTHERWISE THESE ASSETS MIGHT BE DUPLICATED IN IRIS!!!**

type: SERVER

parameters:
  - name: clientId
    description: Client Id of the client that should be synced to DFIR-IRIS
  - name: isCompromised
    default: TBD
    description: Specify whether this asset should be marked as compromised in IRIS using "Y" (compromised), "N" (not compromised), "TBD" (to be determined), or "UNK" (unknown).
    type: choices
    choices:
      - Y
      - N
      - TBD
      - UNK
  - name: labelIgnoreListRegex
    default: "IRIS|^Workstation$|^Server$|^Domain Controller$|^Linux$"
    description: Labels that should be ignored and not added to IRIS
  - name: IrisURL
    type: server_metadata
    description: URL of DFIR-IRIS. Preferred method is to use the server metadata
  - name: IrisKey
    type: server_metadata
    description: API Key of DFIR-IRIS. Preferred method is to use the server metadata
  - name: IrisCaseId
    type: server_metadata
    description: Case ID of the current case. Preferred method is to use the server metadata
  - name: IrisRootCA
    type: server_metadata
    description: RootCA of DFIR-IRIS for self-signed or internal certificates of DFIR-IRIS. Preferred over completely skipping SSL verification.
  - name: DisableSSLVerify
    type: bool
    default: false
    description: Disable TLS verification for HTTPS request to DFIR-IRIS.

sources:
  - query: |

      LET AssetType = SELECT * FROM switch(
        a = {SELECT {
                SELECT if(condition = `Computer Info`.DomainRole =~ "Workstation",
                    then = 9,
                    else = if(condition= `Computer Info`.DomainRole =~ "Server",
                        then = 10,
                        else = if(condition= `Computer Info`.DomainRole =~ "Domain Controller",
                            then = 11
                    )))
                FROM flow_results(client_id=clientId,flow_id=last_interrogate_flow_id,artifact="Generic.Client.Info/WindowsInfo")
              } as AssetTypeId
              FROM clients(client_id=clientId)
              WHERE os_info.system =~ "windows"
        },
        b = {SELECT 3 as AssetTypeId
              FROM clients(client_id=clientId)
              WHERE os_info.system =~ "linux"})

      LET resolveIPs =
          SELECT
            {SELECT `Network Info` FROM flow_results(client_id=client_id,flow_id=last_interrogate_flow_id,artifact="Generic.Client.Info/WindowsInfo")} as NetworkInfo
          FROM clients(client_id=clientId)

      LET primaryIP =
          SELECT parse_string_with_regex(string=if(condition=NetworkInfo[0],then=NetworkInfo[0].IPAddresses,else=NetworkInfo.IPAddresses),regex="(?P<IP>[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})").IP as PrimaryIPv4Address
          FROM resolveIPs

      LET networkInterfaces =
          SELECT parse_string_with_regex(string=NetworkInfo.Caption,regex="^\\[.*\\] (?P<IF>.*)").IF as NetworkInterface,
                            NetworkInfo.IPAddresses as IPAddresses, NetworkInfo.MACAddress as MACAddress
          FROM flatten(query=resolveIPs)

      LET NetInfo =
          SELECT format(format="%v (%v): %v",args=[NetworkInterface,MACAddress,IPAddresses]) AS fmt FROM networkInterfaces

      LET networkInterfacesDescription =
          SELECT join(array=NetInfo.fmt, sep="\n") as NetworkInfo
          FROM NetInfo

      LET labelToTags =
          join(array=filter(list=labels, condition="x=>NOT x =~ labelIgnoreListRegex"),sep=",")

      LET metadata_preparation =
          SELECT client_metadata(client_id=clientId) as metadata FROM scope() WHERE metadata.IRIS_AssetId

      LET assetId =
          SELECT metadata_preparation.metadata[0].IRIS_AssetId as assetId FROM scope()

      LET addMetadata(assetIdValue) =
          SELECT client_set_metadata(client_id=clientId,metadata=client_metadata(client_id=clientId) + dict(IRIS_AssetId=assetIdValue)), client_metadata(client_id=clientId)
          FROM scope()

      LET assetProperties =
          serialize(item=dict(
                    asset_name=format(format="%v",args=[os_info.hostname]),
                    asset_type_id=AssetType.AssetTypeId[0],
                    analysis_status_id=1,
                    asset_compromise_status_id=if(condition=isCompromised=~"^Y$",then=1,else=if(condition=isCompromised=~"^N$",then=2,else=if(condition=isCompromised=~"^UNK$",then=3,else=0))),
                    asset_domain=format(format="%v",args=[join(array=slice(list=split(sep_string=".",string=os_info.fqdn),start=1,end=-1),sep=".")]),
                    asset_ip=format(format="%v",args=[primaryIP.PrimaryIPv4Address]),
                    asset_tags=if(condition=labelToTags,then=format(format="Velo,%v",args=[labelToTags]),else="Velo"),
                    asset_description=format(format="Velo ClientId: %v\nVelo Agent First seen: %v\nAsset added to IRIS by Velo: %v\nNetwork Info:\n%v", args=[client_id,timestamp(epoch=first_seen_at),timestamp(epoch=now()),networkInterfacesDescription.NetworkInfo[0]])
                ), format="json" )

      LET apiRequestIrisAdd =
          SELECT *, if(condition=parse_json(data=Content).data.asset_id,
                    then={ SELECT addMetadata(assetIdValue=format(format="%v",
                                  args=parse_json(data=Content).data.asset_id)),
                                  label(client_id=clientId,op="set",labels="IRIS"),
                                  label(client_id=clientId,op="remove",labels="IRIS-ERROR")
                           FROM scope()},
                    else={ SELECT label(client_id=clientId,op="set",labels="IRIS-ERROR")
                           FROM scope()}) as applyLabels
          FROM http_client(
                 data=assetProperties,
                 headers=dict(
                 `Content-Type`="application/json", `Authorization`=format(format="Bearer %v",
                   args=[IrisKey])),
                 skip_verify=DisableSSLVerify,
                 root_ca=IrisRootCA,
                 method="POST",
                 url=format(format="%v/case/assets/add?cid=%v", args=[IrisURL,IrisCaseId]))

      LET apiRequestIrisGet(assetId) =
          SELECT parse_json(data=Content),
                 parse_json(data=Content).data.asset_name as asset_name,
                 parse_json(data=Content).data.asset_type_id as asset_type_id,
                 parse_json(data=Content).data.analysis_status_id as analysis_status_id,
                 parse_json(data=Content).data.asset_tags as asset_tags,
                 parse_json(data=Content).data.linked_ioc as linked_ioc,
                 parse_json(data=Content).data.custom_attributes as custom_attributes,
                 parse_json(data=Content).data.asset_compromise_status_id as asset_compromise_status_id
          FROM http_client(
                  headers=dict(
                     `Content-Type`="application/json", `Authorization`=format(format="Bearer %v",
                  args=[IrisKey])),
                  skip_verify=DisableSSLVerify,
                  root_ca=IrisRootCA,
                  method="GET",
                  url=format(format="%v/case/assets/%v?cid=%v", args=[IrisURL,assetId,IrisCaseId]))

      LET assetPropertiesUpdate = serialize(
          item=dict(
            asset_name=currentAsset.asset_name[0],
            asset_type_id=currentAsset.asset_type_id[0],
            analysis_status_id=currentAsset.analysis_status_id[0],
            asset_compromise_status_id=if(
              condition=isCompromised =~ "^Y$",
              then=1,
              else=if(
                condition=currentAsset.asset_compromise_status_id[0] = 1,
                then=1,
                else=if(
                  condition=isCompromised =~ "^N$",
                  then=2,
                  else=if(
                    condition=isCompromised =~ "^UNK$",
                    then=3,
                    else=if(
                      condition=isCompromised =~ "^TBD$",
                        then=0,
                        else=currentAsset.asset_compromise_status_id[0]))))),
            asset_tags=if(
              condition=labelToTags,
              then=format(format="Velo,%v", args=[labelToTags]),
              else="Velo")),
          format="json")

      LET apiRequestIrisUpdate(currentAsset,assetId) =
          SELECT *,
                 if(condition= Response=200,
                 then={ SELECT addMetadata(assetIdValue=format(format="%v",args=parse_json(data=Content).data.asset_id)),
                              label(client_id=clientId,op="set",labels="IRIS"),
                              label(client_id=clientId,op="remove",labels="IRIS-ERROR")
                        FROM scope()},
                 else={ SELECT label(client_id=clientId,op="set",labels="IRIS-ERROR") FROM scope()}) as applyLabels
                        FROM http_client(data=assetPropertiesUpdate,
                                         headers=dict(
                                           `Content-Type`="application/json", `Authorization`=format(format="Bearer %v",
                                             args=[IrisKey])),
                                         skip_verify=DisableSSLVerify,
                                         root_ca=IrisRootCA,
                                         method="POST",
                                         url=format(format="%v/case/assets/update/%v?cid=%v",
                                                           args=[IrisURL,assetId,IrisCaseId]))

      LET addAsset =
          SELECT { SELECT * FROM apiRequestIrisAdd } as apiRequestIrisAdd FROM clients(client_id=clientId)

      LET updateAsset =
          SELECT { SELECT * FROM apiRequestIrisUpdate(currentAsset=apiRequestIrisGet(assetId=client_metadata(client_id=clientId).IRIS_AssetId),assetId=client_metadata(client_id=clientId).IRIS_AssetId) } as apiRequestIrisUpdate
          FROM clients(client_id=clientId)


      SELECT * FROM if(condition= metadata_preparation,
        then={ SELECT
               "Already Added -> Update labels and compromise status in IRIS" AS Action,
               if(
                 condition=apiRequestIrisUpdate.Response = 200,
                 then="SUCCESS",
                 else="ERROR") AS Result,
               parse_json(
                 data=apiRequestIrisUpdate.Content).data AS AssetProperties,
               apiRequestIrisUpdate.applyLabels[0].`addMetadata(assetIdValue=format(format="%v", args=parse_json(data=Content).data.asset_id))`[0].`client_metadata(client_id=clientId)`.IRIS_AssetId AS IRIS_AssetId,
               apiRequestIrisUpdate AS _rawEvent
            FROM updateAsset
        },
        else={ SELECT
             "Needs to be added" AS Action,
             if(
               condition=apiRequestIrisAdd.Response = 200,
               then="SUCCESS",
               else="ERROR") AS Result,
             parse_json(
               data=apiRequestIrisAdd.Content).data AS AssetProperties,
             apiRequestIrisAdd.applyLabels[0].`addMetadata(assetIdValue=format(format="%v", args=parse_json(data=Content).data.asset_id))`[0].`client_metadata(client_id=clientId)`.IRIS_AssetId AS IRIS_AssetId,
             apiRequestIrisAdd AS _rawEvent
             FROM addAsset
            }
        )