IRIS.Timeline.Add

Adds Velociraptor rows as timeline entries to DFIR-IRIS .

Links the assets and IOCs as specified in the parameters. Additionally, if the client does not yet exist in Iris, this artifact will leverage the IRIS.Sync.Asset artifact to add the asset to Iris first and link it in the event.

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

Notes:

  • The following parameters are mandatory:
    1. Timestamp: This specifies the name of the field in the source containing the event timestamp. For this artifact to parse it correctly the field should contain a parsed timestamp object. If you are using this artifact from a global notebook then the field is probably already parsed. If not then you should ensure that it is parsed in your source using the timestamp function.
    2. Title: This specifies the name of the field in the source containing the event title which will be used on the Iris timeline.

Hints:

  • 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()
  • The true power of this artifact lies in the ability to quickly add many entries to DFIR-IRIS. You will most likely use this artifact from within a notebook.
  • There is a basic mechanism established to stop duplicates from being added. An event is compared to existing entries based on asset name, flow id, timestamp and the description. You can add multiple events happening at the same time for the same asset originating from the same flow as long as the description varies, e.g. by including dynamic details of the activity that differentiates between the events at the same time like a process name.

Notebook usage example:

LET ClientId <= '''C.daa3bab35a125058'''
LET FlowId <= '''F.CPTTPTRO63LF6'''
LET ArtifactName <= '''Windows.Timeline.MFT'''

-- This is the query that should return the events you want to add to Iris.
-- You might want to add a WHERE clause to filter out unwanted events or
-- select only specific fields. In this example we limit it to 10 records.
LET eventsToAdd = SELECT * FROM source(artifact=ArtifactName)
                  LIMIT 10

SELECT * FROM foreach(
  row={
    SELECT to_dict(item=_value) AS event,
           serialize(format="json", item=_value) AS raw_event
    FROM items(item={ SELECT * FROM eventsToAdd })
  },
  query={
    SELECT *
    FROM Artifact.Exchange.IRIS.Timeline.Add(
      AdditionalAssetId="1,2,3",
      AddToGraph=true,
      AddToSummary=false,
      IocId="8,9,10",
      Category="pers",
      clientId=ClientId,
      Description=format(
        format="Malicious file dropped to the system to establish persistence.\nFile path: %v\nActivity: %v",
        args=[event.path, event.message]),
      RawEvent=raw_event,
      DisableSSLVerify=true,
      FlowId=FlowId,
      Timestamp=event.event_time,
      Title="Persistence established via Autostart Location")
  },
  async=false)

ATTENTION: ALWAYS USE ASYNC=FALSE OTHERWISE ANY ASSETS THAT NEED TO BE CREATED MIGHT BE DUPLICATED!!!


name: IRIS.Timeline.Add

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

description: |
   Adds Velociraptor rows as timeline entries to [DFIR-IRIS](https://dfir-iris.org/).

   Links the assets and IOCs as specified in the parameters. Additionally, if the client does not yet exist in Iris, this artifact will leverage the **IRIS.Sync.Asset** artifact to add the asset to Iris first and link it in the event.

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

   #### Notes:

   - The following parameters are *mandatory*:
     1. **Timestamp**: This specifies the name of the field in the source containing the event timestamp. For this artifact to parse it correctly the field should contain a parsed timestamp object. If you are using this artifact from a global notebook then the field is probably already parsed. If not then you should ensure that it is parsed in your source using the `timestamp` function.
     2. **Title**: This specifies the name of the field in the source containing the event title which will be used on the Iris timeline.

   #### Hints:

   - 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()
   ```

   - The true power of this artifact lies in the ability to quickly add many entries to DFIR-IRIS. You will *most likely use this artifact from within a notebook*.
   - There is a basic mechanism established to stop duplicates from being added. An event is compared to existing entries based on asset name, flow id, timestamp and the description. You can add multiple events happening at the same time for the same asset originating from the same flow as long as the description varies, e.g. by including dynamic details of the activity that differentiates between the events at the same time like a process name.

   #### Notebook usage example:

   ```VQL
   LET ClientId <= '''C.daa3bab35a125058'''
   LET FlowId <= '''F.CPTTPTRO63LF6'''
   LET ArtifactName <= '''Windows.Timeline.MFT'''

   -- This is the query that should return the events you want to add to Iris.
   -- You might want to add a WHERE clause to filter out unwanted events or
   -- select only specific fields. In this example we limit it to 10 records.
   LET eventsToAdd = SELECT * FROM source(artifact=ArtifactName)
                     LIMIT 10

   SELECT * FROM foreach(
     row={
       SELECT to_dict(item=_value) AS event,
              serialize(format="json", item=_value) AS raw_event
       FROM items(item={ SELECT * FROM eventsToAdd })
     },
     query={
       SELECT *
       FROM Artifact.Exchange.IRIS.Timeline.Add(
         AdditionalAssetId="1,2,3",
         AddToGraph=true,
         AddToSummary=false,
         IocId="8,9,10",
         Category="pers",
         clientId=ClientId,
         Description=format(
           format="Malicious file dropped to the system to establish persistence.\nFile path: %v\nActivity: %v",
           args=[event.path, event.message]),
         RawEvent=raw_event,
         DisableSSLVerify=true,
         FlowId=FlowId,
         Timestamp=event.event_time,
         Title="Persistence established via Autostart Location")
     },
     async=false)
   ```
   **ATTENTION: ALWAYS USE ASYNC=FALSE OTHERWISE ANY ASSETS THAT NEED TO BE CREATED MIGHT BE DUPLICATED!!!**

# Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT
type: SERVER

parameters:
  - name: clientId
    description: Client Id of the client that should be synced to DFIR-IRIS
  - name: AdditionalAssetId
    description: Comma seperated list of IRIS AssetIds of additional assets beside the client to link in this event.
  - name: IocId
    description: Comma seperated list of IRIS IocIds to link IOCs in this event.
  - name: Timestamp
    description: Timestamp of the event as a time.Time object. This can be a field in the source data containing a timestamp object.
  - name: Title
    description: Title of the event.
  - name: FlowId
    description: FlowId or HuntId of the event source. This is needed to allow detection of duplicates!
  - name: Tags
    description: List of comma seperated tags to be added to the event.
  - name: Description
    description: Description of the event. Very important to actually understand what this entry is all about :)
  - name: AddToSummary
    description: Add it to timeline summary?
    type: bool
  - name: AddToGraph
    description: Add it to attack graph?
    type: bool
  - name: Category
    description: "Category of the action, mostly MITRE Enterprise Tactics. Allowed options are abbreviations and their MITRE ID: tbd,legit,rem,ini,exec,pers,priv,def,creds,disc,lat,coll,c2,exfil,imp"
    type: choices
    choices:
      - tbd
      - legit
      - rem
      - ini
      - exec
      - pers
      - priv
      - def
      - creds
      - disc
      - lat
      - coll
      - c2
      - exfil
      - imp
  - name: Color
    description: Specify the color for this event in Iris. Green by default for obvious reasons.
    type: choices
    choices:
      - green
      - white
      - blue
      - lightblue
      - purple
      - red
      - orange
  - name: RawEvent
    description: Add the raw event, message or the entire row as additional information.
  - 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 metadata_preparation = SELECT client_metadata(client_id=clientId) as metadata FROM scope() WHERE metadata.IRIS_AssetId

      LET syncAsset = SELECT * FROM Artifact.Exchange.IRIS.Sync.Asset(clientId=clientId,IrisURL=IrisURL,IrisCaseId=IrisCaseId,IrisKey=IrisKey,IrisRootCA=IrisRootCA,DisableSSLVerify=DisableSSLVerify)

      LET eventAsset1 = if(condition=metadata_preparation,then=array(a=metadata_preparation.metadata[0].IRIS_AssetId),else=if(condition=syncAsset.Result[0]="SUCCESS",then=array(a=metadata_preparation.metadata[0].IRIS_AssetId),else=[]))

      LET eventAsset2 = if(condition=AdditionalAssetId,then=split(string=AdditionalAssetId,sep=",|;"),else=[])

      LET eventCategory = if(condition=Category=~"^legit",then=2,
                    else= if(condition=Category=~"^rem",then=3,
                    else= if(condition=Category=~"^ini|^ta0001$",then=4,
                    else= if(condition=Category=~"^exec|^ta0002$",then=5,
                    else= if(condition=Category=~"^pers|^ta0003$",then=6,
                    else= if(condition=Category=~"^priv|^ta0004$",then=7,
                    else= if(condition=Category=~"^def|^ta0005$",then=8,
                    else= if(condition=Category=~"^cred|^ta0006$",then=9,
                    else= if(condition=Category=~"^disc|^ta0007$",then=10,
                    else= if(condition=Category=~"^lat|^ta0008$",then=11,
                    else= if(condition=Category=~"^coll|^ta0009$",then=12,
                    else= if(condition=Category=~"^c2|^com|^ta0011$",then=13,
                    else= if(condition=Category=~"^exf|^ta0010$",then=14,
                    else= if(condition=Category=~"^imp|^ta0040$",then=15,
                    else= 1))))))))))))))

      LET eventColor = if(condition=Color =~ "^white",then="#fff",
                else = if(condition=Color =~ "^blue",then="#1572E899",
                else = if(condition=Color =~ "^purple",then="#6861CE99",
                else = if(condition=Color =~ "^lightblue",then="#48ABF799",
                else = if(condition=Color =~ "^red",then="#F2596199",
                else = if(condition=Color =~ "^orange",then="#FFAD4699",
                else = "#31CE3699"))))))

      LET eventDate = format(format="%d-%02d-%02dT%02d:%02d:%02d.%03.f", args=[
                                      Timestamp.Year, Timestamp.Month, Timestamp.Day,
                                      Timestamp.Hour, Timestamp.Minute, Timestamp.Second,
                                      Timestamp.Nanosecond / 1000000
                            ])

      LET eventProperties = serialize(
                            item=dict(
                                event_title=Title,
                                event_source=if(condition=FlowId,then=format(format="Velo: %v",args=[FlowId]),else="Velo"),
                                event_assets=if(condition=eventAsset1 OR eventAsset2,then=eventAsset1 + eventAsset2,else=[]),
                                event_iocs=if(condition=IocId,then=split(string=IocId,sep=",|;"),else=[]),
                                event_tags=if(condition=Tags,then=format(format="Velo,%v",args=[Tags]),else="Velo"),
                                event_category_id=eventCategory,
                                event_in_summary=AddToSummary,
                                event_in_graph=AddToGraph,
                                event_color=eventColor,
                                event_date=eventDate,
                                event_tz="+00:00",
                                event_content=Description,
                                event_raw=RawEvent
                            )
                            ,format="json"
                        )

      LET apiRequestIrisAddEvent =
          SELECT *
          FROM http_client(
              data=eventProperties,
              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/timeline/events/add?cid=%v", args=[IrisURL,IrisCaseId]))

      LET resolveHostname = SELECT os_info.hostname as hostname from clients(client_id=clientId)

      LET filterParams = dict(cid=IrisCaseId,q=format(format='{"asset":["%v"],"source":["%v"],"startDate":["%v"],"endDate":["%v"]}',args=[resolveHostname.hostname[0],FlowId,eventDate,eventDate]))

      LET checkExistingEntries =
          SELECT * FROM flatten(query={ SELECT parse_json(data=Content).data.timeline as Timeline
                                        FROM http_client(
                                            headers=dict(`Content-Type`="application/json", `Authorization`=format(format="Bearer %v", args=[IrisKey])),
                                            method="GET",
                                            root_ca=IrisRootCA,
                                            skip_verify=DisableSSLVerify,
                                            params=filterParams,
                                            url=format(format="%v/case/timeline/advanced-filter", args=[IrisURL])) GROUP BY Timeline })
                                       WHERE base64encode(string=Timeline.event_content) = base64encode(string=Description)

      SELECT * FROM if(condition=checkExistingEntries,
                then={SELECT "Already Added -> Skipping the event. Check for existing entries manually!" as Action FROM scope()},
                else={SELECT "Needs to be added" as Action, if(condition= Response=200,then="SUCCESS",else="ERROR") as Result,
                      parse_json(data=eventProperties) AS _RequestData, parse_json(data=Content).data as _ResponseData
                      FROM apiRequestIrisAddEvent})